diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index dbb6104ce..bd325d56b 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 +Subproject commit bd325d56b4c62c9c5c1aff048c37c6bb18ac0290 diff --git a/src/contracts/examples/ex-post-mev-example/SwapMath.sol b/src/contracts/examples/ex-post-mev-example/SwapMath.sol deleted file mode 100644 index 896dd21e3..000000000 --- a/src/contracts/examples/ex-post-mev-example/SwapMath.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -library SwapMath { - function getAmountIn( - uint256 amountOut, - uint256 reservesIn, - uint256 reservesOut - ) - internal - pure - returns (uint256 amountIn) - { - uint256 numerator = reservesIn * amountOut * 1000; - uint256 denominator = (reservesOut - amountOut) * 997; - amountIn = (numerator / denominator) + 1; - } -} diff --git a/src/contracts/examples/ex-post-mev-example/V2ExPost.sol b/src/contracts/examples/ex-post-mev-example/V2ExPost.sol deleted file mode 100644 index 6d7bb4948..000000000 --- a/src/contracts/examples/ex-post-mev-example/V2ExPost.sol +++ /dev/null @@ -1,145 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Base Imports -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; - -import { SafetyBits } from "src/contracts/libraries/SafetyBits.sol"; - -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Atlas DApp-Control Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; - -// Uni V2 Imports -import { IUniswapV2Pair } from "./interfaces/IUniswapV2Pair.sol"; -import { IUniswapV2Factory } from "./interfaces/IUniswapV2Factory.sol"; - -// Misc -import { SwapMath } from "./SwapMath.sol"; - -// import "forge-std/Test.sol"; - -interface IWETH { - function deposit() external payable; - function withdraw(uint256 wad) external; -} - -contract V2ExPost is DAppControl { - address public constant WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - - event GiftedGovernanceToken(address indexed user, address indexed token, uint256 amount); - - constructor(address _atlas) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: true, - trackPreOpsReturnData: false, - trackUserReturnData: false, - delegateUser: false, - requirePreSolver: false, - requirePostSolver: false, - requirePostOps: false, - zeroSolvers: true, - reuseUserOp: false, - userAuctioneer: true, - solverAuctioneer: true, - unknownAuctioneer: true, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: false, - trustedOpHash: false, - invertBidValue: false, - exPostBids: true, - allowAllocateValueFailure: false - }) - ) - { } - - function _checkUserOperation(UserOperation memory userOp) internal view override { - require(bytes4(userOp.data) == IUniswapV2Pair.swap.selector, "ERR-H10 InvalidFunction"); - require( - IUniswapV2Factory(IUniswapV2Pair(userOp.dapp).factory()).getPair( - IUniswapV2Pair(userOp.dapp).token0(), IUniswapV2Pair(userOp.dapp).token1() - ) == userOp.dapp, - "ERR-H11 Invalid pair" - ); - } - - function _preOpsCall(UserOperation calldata userOp) internal override returns (bytes memory returnData) { - ( - uint256 amount0Out, - uint256 amount1Out, - , // address recipient // Unused - // bytes memory swapData // Unused - ) = abi.decode(userOp.data[4:], (uint256, uint256, address, bytes)); - - require(amount0Out == 0 || amount1Out == 0, "ERR-H12 InvalidAmountOuts"); - require(amount0Out > 0 || amount1Out > 0, "ERR-H13 InvalidAmountOuts"); - - (uint112 token0Balance, uint112 token1Balance,) = IUniswapV2Pair(userOp.dapp).getReserves(); - - uint256 amount0In = - amount1Out == 0 ? 0 : SwapMath.getAmountIn(amount1Out, uint256(token0Balance), uint256(token1Balance)); - uint256 amount1In = - amount0Out == 0 ? 0 : SwapMath.getAmountIn(amount0Out, uint256(token1Balance), uint256(token0Balance)); - - // This is a V2 swap, so optimistically transfer the tokens - // NOTE: The user should have approved Atlas for token transfers - _transferUserERC20( - amount0Out > amount1Out ? IUniswapV2Pair(userOp.dapp).token1() : IUniswapV2Pair(userOp.dapp).token0(), - userOp.dapp, - amount0In > amount1In ? amount0In : amount1In - ); - - return new bytes(0); - } - - /* - * @notice This function is called after a solver has successfully paid their bid - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev transfers the bid amount to the user supports only WETH - * @param bidToken The address of the token used for the winning SolverOperation's bid - * @param bidAmount The winning bid amount - * @param _ - */ - function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata) internal override { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Escrow - require(bidToken == WETH, "V2ExPost: InvalidBidToken"); - SafeTransferLib.safeTransfer(bidToken, _user(), bidAmount); - - /* - // ENABLE FOR FOUNDRY TESTING - console.log("----====++++====----"); - console.log("DApp Control"); - console.log("Governance Tokens Burned:", govIsTok0 ? amount0Out : amount1Out); - console.log("----====++++====----"); - */ - } - - ///////////////// GETTERS & HELPERS // ////////////////// - - function getBidFormat(UserOperation calldata) public pure override returns (address bidToken) { - // This is a helper function called by solvers - // so that they can get the proper format for - // submitting their bids to the hook. - return WETH; - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } -} diff --git a/src/contracts/examples/ex-post-mev-example/interfaces/IUniswapV2Factory.sol b/src/contracts/examples/ex-post-mev-example/interfaces/IUniswapV2Factory.sol deleted file mode 100644 index d9e676e92..000000000 --- a/src/contracts/examples/ex-post-mev-example/interfaces/IUniswapV2Factory.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.5.0; - -interface IUniswapV2Factory { - event PairCreated(address indexed token0, address indexed token1, address pair, uint256); - - function feeTo() external view returns (address); - function feeToSetter() external view returns (address); - - function getPair(address tokenA, address tokenB) external view returns (address pair); - function allPairs(uint256) external view returns (address pair); - function allPairsLength() external view returns (uint256); - - function createPair(address tokenA, address tokenB) external returns (address pair); - - function setFeeTo(address) external; - function setFeeToSetter(address) external; -} diff --git a/src/contracts/examples/ex-post-mev-example/interfaces/IUniswapV2Pair.sol b/src/contracts/examples/ex-post-mev-example/interfaces/IUniswapV2Pair.sol deleted file mode 100644 index 751eea813..000000000 --- a/src/contracts/examples/ex-post-mev-example/interfaces/IUniswapV2Pair.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.5.0; - -interface IUniswapV2Pair { - event Approval(address indexed owner, address indexed spender, uint256 value); - event Transfer(address indexed from, address indexed to, uint256 value); - - function name() external pure returns (string memory); - function symbol() external pure returns (string memory); - function decimals() external pure returns (uint8); - function totalSupply() external view returns (uint256); - function balanceOf(address owner) external view returns (uint256); - function allowance(address owner, address spender) external view returns (uint256); - - function approve(address spender, uint256 value) external returns (bool); - function transfer(address to, uint256 value) external returns (bool); - function transferFrom(address from, address to, uint256 value) external returns (bool); - - function nonces(address owner) external view returns (uint256); - - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) - external; - - event Mint(address indexed sender, uint256 amount0, uint256 amount1); - event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); - event Swap( - address indexed sender, - uint256 amount0In, - uint256 amount1In, - uint256 amount0Out, - uint256 amount1Out, - address indexed to - ); - event Sync(uint112 reserve0, uint112 reserve1); - - function factory() external view returns (address); - function token0() external view returns (address); - function token1() external view returns (address); - function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); - function price0CumulativeLast() external view returns (uint256); - function price1CumulativeLast() external view returns (uint256); - function kLast() external view returns (uint256); - - function mint(address to) external returns (uint256 liquidity); - function burn(address to) external returns (uint256 amount0, uint256 amount1); - function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; - function skim(address to) external; - function sync() external; - - function initialize(address, address) external; -} diff --git a/src/contracts/examples/fastlane-online/BaseStorage.sol b/src/contracts/examples/fastlane-online/BaseStorage.sol deleted file mode 100644 index d552082ea..000000000 --- a/src/contracts/examples/fastlane-online/BaseStorage.sol +++ /dev/null @@ -1,110 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -import "src/contracts/types/SolverOperation.sol"; - -import { Reputation } from "src/contracts/examples/fastlane-online/FastLaneTypes.sol"; - -contract BaseStorage { - error FLOnline_NotUnlocked(); - - // OK hear me out - // 1. We have to rake some of the congestion buyins to maintain incentive compatibility (a low rep solver smurfing - // as a user to collect - // congestion fees from competing solvers by bidding extremely high and knowing they'll get 100% of it back) - // 2. Oval charges 50% of all OEV and we're just charging 33% of *only* the congestion fees - not MEV - which means - // we're practically saints. - uint256 internal constant _CONGESTION_RAKE = 33_000; - uint256 internal constant _CONGESTION_BASE = 100_000; - bytes32 private constant _USER_LOCK_SLOT = keccak256("FLO_USER_LOCK"); - bytes32 private constant _WINNING_SOLVER_SLOT = keccak256("FLO_WINNING_SOLVER"); - - uint256 internal S_rake; - - // SolverOpHash SolverOperation - mapping(bytes32 => SolverOperation) internal S_solverOpCache; - - // UserOpHash SolverOpHash[] - mapping(bytes32 => bytes32[]) internal S_solverOpHashes; - - // SolverOpHash BidValue - mapping(bytes32 => uint256) internal S_congestionBuyIn; - - // UserOpHash TotalBidValue - mapping(bytes32 => uint256) internal S_aggCongestionBuyIn; - - // SolverFrom Reputation - mapping(address => Reputation) internal S_solverReputations; - - ////////////////////////////////////////////// - ///// VIEW FUNCTIONS ////// - ////////////////////////////////////////////// - - function rake() external view returns (uint256) { - return S_rake; - } - - function solverOpCache(bytes32 solverOpHash) external view returns (SolverOperation memory) { - return S_solverOpCache[solverOpHash]; - } - - function solverOpHashes(bytes32 userOpHash) external view returns (bytes32[] memory) { - return S_solverOpHashes[userOpHash]; - } - - function congestionBuyIn(bytes32 solverOpHash) external view returns (uint256) { - return S_congestionBuyIn[solverOpHash]; - } - - function aggCongestionBuyIn(bytes32 userOpHash) external view returns (uint256) { - return S_aggCongestionBuyIn[userOpHash]; - } - - function solverReputation(address solver) external view returns (Reputation memory) { - return S_solverReputations[solver]; - } - - ////////////////////////////////////////////// - ///// MODIFIERS ////// - ////////////////////////////////////////////// - - modifier withUserLock(address user) { - if (_getUserLock() != address(0)) revert FLOnline_NotUnlocked(); - _setUserLock(user); - _; - _setUserLock(address(0)); - } - - ////////////////////////////////////////////// - ///// TSTORE HELPERS ////// - ////////////////////////////////////////////// - - function _setUserLock(address user) internal { - _tstore(_USER_LOCK_SLOT, bytes32(uint256(uint160(user)))); - } - - function _getUserLock() internal view returns (address) { - return address(uint160(uint256(_tload(_USER_LOCK_SLOT)))); - } - - function _setWinningSolver(address winningSolverFrom) internal { - _tstore(_WINNING_SOLVER_SLOT, bytes32(uint256(uint160(winningSolverFrom)))); - } - - function _getWinningSolver() internal view returns (address) { - return address(uint160(uint256(_tload(_WINNING_SOLVER_SLOT)))); - } - - function _tstore(bytes32 slot, bytes32 value) internal { - assembly { - tstore(slot, value) - } - } - - function _tload(bytes32 slot) internal view returns (bytes32 value) { - assembly { - value := tload(slot) - } - return value; - } -} diff --git a/src/contracts/examples/fastlane-online/FastLaneControl.sol b/src/contracts/examples/fastlane-online/FastLaneControl.sol deleted file mode 100644 index 0166f2d6e..000000000 --- a/src/contracts/examples/fastlane-online/FastLaneControl.sol +++ /dev/null @@ -1,212 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; - -import { SwapIntent, BaselineCall } from "src/contracts/examples/fastlane-online/FastLaneTypes.sol"; -import { FastLaneOnlineErrors } from "src/contracts/examples/fastlane-online/FastLaneOnlineErrors.sol"; -import { IFastLaneOnline } from "src/contracts/examples/fastlane-online/IFastLaneOnline.sol"; - -interface ISolverGateway { - function getBidAmount(bytes32 solverOpHash) external view returns (uint256 bidAmount); -} - -contract FastLaneOnlineControl is DAppControl, FastLaneOnlineErrors { - address internal constant _NATIVE_TOKEN = address(0); - - constructor(address _atlas) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: false, - trackPreOpsReturnData: false, - trackUserReturnData: true, - delegateUser: true, - requirePreSolver: true, - requirePostSolver: false, - requirePostOps: true, - zeroSolvers: true, - reuseUserOp: true, - userAuctioneer: true, - solverAuctioneer: false, - unknownAuctioneer: false, - verifyCallChainHash: false, - forwardReturnData: true, - requireFulfillment: false, - trustedOpHash: false, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { } - - // ---------------------------------------------------- // - // Atlas hooks // - // ---------------------------------------------------- // - - /* - * @notice This function is called before a solver operation executes - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It transfers the tokens that the user is selling to the solver - * @param solverOp The SolverOperation that is about to execute - * @return true if the transfer was successful, false otherwise - */ - function _preSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override { - (SwapIntent memory _swapIntent,) = abi.decode(returnData, (SwapIntent, BaselineCall)); - - // Make sure the token is correct - if (solverOp.bidToken != _swapIntent.tokenUserBuys) { - revert FLOnlineControl_PreSolver_BuyTokenMismatch(); - } - if (solverOp.bidToken == _swapIntent.tokenUserSells) { - revert FLOnlineControl_PreSolver_SellTokenMismatch(); - } - - // NOTE: This module is unlike the generalized swap intent module - here, the solverOp.bidAmount includes - // the min amount that the user expects. - // We revert early if the baseline swap returned more than the solver's bid. - if (solverOp.bidAmount < _swapIntent.minAmountUserBuys) { - revert FLOnlineControl_PreSolver_BidBelowReserve(); - } - - // Optimistically transfer the user's sell tokens to the solver. - if (_swapIntent.tokenUserSells == _NATIVE_TOKEN) { - SafeTransferLib.safeTransferETH(solverOp.solver, _swapIntent.amountUserSells); - } else { - SafeTransferLib.safeTransfer(_swapIntent.tokenUserSells, solverOp.solver, _swapIntent.amountUserSells); - } - return; // success - } - - /* - * @notice This function is called after a solver has successfully paid their bid - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It transfers all the available bid tokens on the contract (instead of only the bid amount, - * to avoid leaving any dust on the contract) - * @param bidToken The address of the token used for the winning solver operation's bid - * @param _ - * @param _ - */ - function _allocateValueCall(address, uint256, bytes calldata returnData) internal override { - (SwapIntent memory _swapIntent,) = abi.decode(returnData, (SwapIntent, BaselineCall)); - _sendTokensToUser(_swapIntent); - } - - function _postOpsCall(bool solved, bytes calldata returnData) internal override { - // If a solver beat the baseline and the amountOutMin, return early - if (solved) { - (address _winningSolver,,) = IAtlas(ATLAS).solverLockData(); - IFastLaneOnline(CONTROL).setWinningSolver(_winningSolver); - return; - } - - (SwapIntent memory _swapIntent, BaselineCall memory _baselineCall) = - abi.decode(returnData, (SwapIntent, BaselineCall)); - - // Do the baseline call - uint256 _buyTokensReceived = _baselineSwap(_swapIntent, _baselineCall); - - // Verify that it exceeds the minAmountOut - if (_buyTokensReceived < _swapIntent.minAmountUserBuys) { - revert FLOnlineControl_PostOpsCall_InsufficientBaseline(); - } - - // Undo the token approval, if not native token. - if (_swapIntent.tokenUserSells != _NATIVE_TOKEN) { - SafeTransferLib.safeApprove(_swapIntent.tokenUserSells, _baselineCall.to, 0); - } - - // Transfer tokens to user - _sendTokensToUser(_swapIntent); - } - - ////////////////////////////////////////////// - // CUSTOM FUNCTIONS // - ////////////////////////////////////////////// - function _sendTokensToUser(SwapIntent memory swapIntent) internal { - // Transfer the buy token - if (swapIntent.tokenUserBuys == _NATIVE_TOKEN) { - SafeTransferLib.safeTransferETH(_user(), address(this).balance); - } else { - SafeTransferLib.safeTransfer(swapIntent.tokenUserBuys, _user(), _getERC20Balance(swapIntent.tokenUserBuys)); - } - - // Transfer any surplus sell token - if (swapIntent.tokenUserSells == _NATIVE_TOKEN) { - SafeTransferLib.safeTransferETH(_user(), address(this).balance); - } else { - SafeTransferLib.safeTransfer( - swapIntent.tokenUserSells, _user(), _getERC20Balance(swapIntent.tokenUserSells) - ); - } - } - - function _baselineSwap( - SwapIntent memory swapIntent, - BaselineCall memory baselineCall - ) - internal - returns (uint256 received) - { - // Track the balance (count any previously-forwarded tokens) - uint256 _startingBalance = swapIntent.tokenUserBuys == _NATIVE_TOKEN - ? address(this).balance - msg.value - : _getERC20Balance(swapIntent.tokenUserBuys); - - // CASE not native token - // NOTE: if native token, pass as value - if (swapIntent.tokenUserSells != _NATIVE_TOKEN) { - // Approve the router (NOTE that this approval happens either inside the try/catch and is reverted - // or in the postOps hook where we cancel it afterwards. - SafeTransferLib.safeApprove(swapIntent.tokenUserSells, baselineCall.to, swapIntent.amountUserSells); - } - - // Perform the Baseline Call - (bool _success,) = baselineCall.to.call{ value: baselineCall.value }(baselineCall.data); - // dont pass custom errors - if (!_success) revert FLOnlineControl_BaselineSwap_BaselineCallFail(); - - // Track the balance delta - uint256 _endingBalance = swapIntent.tokenUserBuys == _NATIVE_TOKEN - ? address(this).balance - msg.value - : _getERC20Balance(swapIntent.tokenUserBuys); - - // dont pass custom errors - if (_endingBalance <= _startingBalance) revert FLOnlineControl_BaselineSwap_NoBalanceIncrease(); - - return _endingBalance - _startingBalance; - } - - // ---------------------------------------------------- // - // Getters and helpers // - // ---------------------------------------------------- // - - function getBidFormat(UserOperation calldata userOp) public pure override returns (address bidToken) { - (SwapIntent memory _swapIntent,) = abi.decode(userOp.data[4:], (SwapIntent, BaselineCall)); - bidToken = _swapIntent.tokenUserBuys; - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } - - function _getERC20Balance(address token) internal view returns (uint256 balance) { - (bool _success, bytes memory _data) = token.staticcall(abi.encodeCall(IERC20.balanceOf, address(this))); - if (!_success) revert FLOnlineControl_BalanceCheckFail(); - balance = abi.decode(_data, (uint256)); - } -} diff --git a/src/contracts/examples/fastlane-online/FastLaneOnlineErrors.sol b/src/contracts/examples/fastlane-online/FastLaneOnlineErrors.sol deleted file mode 100644 index 603cdff3a..000000000 --- a/src/contracts/examples/fastlane-online/FastLaneOnlineErrors.sol +++ /dev/null @@ -1,46 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -contract FastLaneOnlineErrors { - // FastLaneControl.sol - error FLOnlineControl_PreSolver_BuyTokenMismatch(); - error FLOnlineControl_PreSolver_SellTokenMismatch(); - error FLOnlineControl_PreSolver_BidBelowReserve(); - - error FLOnlineControl_PostOpsCall_InsufficientBaseline(); - - error FLOnlineControl_BaselineSwap_BaselineCallFail(); - error FLOnlineControl_BaselineSwap_NoBalanceIncrease(); - - error FLOnlineControl_BalanceCheckFail(); - - // FastLaneOnlineInner.sol - error FLOnlineInner_Swap_OnlyAtlas(); - error FLOnlineInner_Swap_MustBeDelegated(); - error FLOnlineInner_Swap_BuyAndSellTokensAreSame(); - error FLOnlineInner_Swap_ControlNotBundler(); - error FLOnlineInner_Swap_UserOpValueTooLow(); - error FLOnlineInner_Swap_BaselineCallValueTooLow(); - - // SolverGateway.sol - error SolverGateway_AddSolverOp_SolverMustBeSender(); - error SolverGateway_AddSolverOp_BidTooHigh(); - error SolverGateway_AddSolverOp_SimulationFail(); - error SolverGateway_AddSolverOp_ScoreTooLow(); - - error SolverGateway_RefundCongestionBuyIns_DeadlineNotPassed(); - - // OuterHelpers.sol - error OuterHelpers_NotMadJustDisappointed(); - - // FLOnlineOuter.sol - error FLOnlineOuter_FastOnlineSwap_NoFulfillment(); - - error FLOnlineOuter_ValidateSwap_InvalidSender(); - error FLOnlineOuter_ValidateSwap_TxGasTooHigh(); - error FLOnlineOuter_ValidateSwap_TxGasTooLow(); - error FLOnlineOuter_ValidateSwap_GasLimitTooLow(); - error FLOnlineOuter_ValidateSwap_MsgValueTooLow(); - error FLOnlineOuter_ValidateSwap_UserOpValueTooLow(); - error FLOnlineOuter_ValidateSwap_UserOpBaselineValueMismatch(); -} diff --git a/src/contracts/examples/fastlane-online/FastLaneOnlineInner.sol b/src/contracts/examples/fastlane-online/FastLaneOnlineInner.sol deleted file mode 100644 index 08d35946d..000000000 --- a/src/contracts/examples/fastlane-online/FastLaneOnlineInner.sol +++ /dev/null @@ -1,134 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { DAppOperation } from "src/contracts/types/DAppOperation.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Interface Import -import { IAtlasVerification } from "src/contracts/interfaces/IAtlasVerification.sol"; -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; - -import { FastLaneOnlineControl } from "src/contracts/examples/fastlane-online/FastLaneControl.sol"; -import { BaseStorage } from "src/contracts/examples/fastlane-online/BaseStorage.sol"; - -import { SwapIntent, BaselineCall } from "src/contracts/examples/fastlane-online/FastLaneTypes.sol"; - -interface IGeneralizedBackrunProxy { - function getUser() external view returns (address); -} - -contract FastLaneOnlineInner is BaseStorage, FastLaneOnlineControl { - error BaselineFailSuccessful(uint256 baselineAmount); - error BaselineFailFailure(); - - event BaselineEstablished(uint256 userMinAmountOut, uint256 baselineAmountOut); - - constructor(address atlas) FastLaneOnlineControl(atlas) { } - - ///////////////////////////////////////////////////////// - // EXECUTION ENVIRONMENT FUNCTIONS // - // // - ///////////////////////////////////////////////////////// - - /* - * @notice This is the user operation target function - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev selector = ?? - * @dev It checks that the user has approved Atlas to spend the tokens they are selling - * @param swapIntent The SwapIntent struct - * @return swapIntent The SwapIntent struct - */ - function swap( - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall - ) - external - payable - returns (SwapIntent memory, BaselineCall memory) - { - if (msg.sender != ATLAS) { - revert FLOnlineInner_Swap_OnlyAtlas(); - } - if (address(this) == CONTROL) { - revert FLOnlineInner_Swap_MustBeDelegated(); - } - if (swapIntent.tokenUserSells == swapIntent.tokenUserBuys) { - revert FLOnlineInner_Swap_BuyAndSellTokensAreSame(); - } - - // control == bundler != user - if (_bundler() != CONTROL) { - revert FLOnlineInner_Swap_ControlNotBundler(); - } - - // Transfer sell token if it isn't native token and validate value deposit if it is - if (swapIntent.tokenUserSells != _NATIVE_TOKEN) { - _transferUserERC20(swapIntent.tokenUserSells, address(this), swapIntent.amountUserSells); - } else { - // UserOp.value already passed to this contract - ensure that userOp.value matches sell amount - if (msg.value < swapIntent.amountUserSells) revert FLOnlineInner_Swap_UserOpValueTooLow(); - if (baselineCall.value < swapIntent.amountUserSells) revert FLOnlineInner_Swap_BaselineCallValueTooLow(); - } - - // Calculate the baseline swap amount from the frontend-sourced routing - // This will typically be a uniswap v2 or v3 path. - // NOTE: This runs inside a try/catch and is reverted. - uint256 _baselineAmount = _catchSwapBaseline(swapIntent, baselineCall); - - emit BaselineEstablished(swapIntent.minAmountUserBuys, _baselineAmount); - - // Update the minAmountUserBuys with this value - // NOTE: If all of the solvers fail to exceed this value, we'll redo this swap in the postOpsHook - // and verify that the min amount is exceeded. - if (_baselineAmount > swapIntent.minAmountUserBuys) { - SwapIntent memory _swapIntent = swapIntent; - _swapIntent.minAmountUserBuys = _baselineAmount; - return (_swapIntent, baselineCall); - } - return (swapIntent, baselineCall); - } - - function _catchSwapBaseline( - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall - ) - internal - returns (uint256 baselineAmount) - { - (bool _success, bytes memory _data) = - CONTROL.delegatecall(_forward(abi.encodeCall(this.baselineSwapTryCatcher, (swapIntent, baselineCall)))); - - if (_success) revert(); // unreachable - - if (bytes4(_data) == BaselineFailSuccessful.selector) { - // Get the uint256 from the memory array - assembly { - let dataLocation := add(_data, 0x20) - baselineAmount := mload(add(dataLocation, sub(mload(_data), 32))) - } - return baselineAmount; - } - return 0; - } - - function baselineSwapTryCatcher(SwapIntent calldata swapIntent, BaselineCall calldata baselineCall) external { - // Do the baseline swap and get the amount received - uint256 _received = _baselineSwap(swapIntent, baselineCall); - - // Revert gracefully to undo the swap but show the baseline amountOut - // NOTE: This does not check the baseline amount against the user's minimum requirement - // This is to allow solvers a chance to succeed even if the baseline swap has returned - // an unacceptable amount (such as if it were sandwiched to try and nullify the swap). - revert BaselineFailSuccessful(_received); - } -} diff --git a/src/contracts/examples/fastlane-online/FastLaneOnlineOuter.sol b/src/contracts/examples/fastlane-online/FastLaneOnlineOuter.sol deleted file mode 100644 index bae46e1c5..000000000 --- a/src/contracts/examples/fastlane-online/FastLaneOnlineOuter.sol +++ /dev/null @@ -1,115 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { DAppOperation } from "src/contracts/types/DAppOperation.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Interface Import -import { IAtlasVerification } from "src/contracts/interfaces/IAtlasVerification.sol"; -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; - -import { FastLaneOnlineControl } from "src/contracts/examples/fastlane-online/FastLaneControl.sol"; -import { FastLaneOnlineInner } from "src/contracts/examples/fastlane-online/FastLaneOnlineInner.sol"; -import { SolverGateway } from "src/contracts/examples/fastlane-online/SolverGateway.sol"; - -import { SwapIntent, BaselineCall } from "src/contracts/examples/fastlane-online/FastLaneTypes.sol"; - -contract FastLaneOnlineOuter is SolverGateway { - constructor(address atlas, address protocolGuildWallet) SolverGateway(atlas, protocolGuildWallet) { } - - ////////////////////////////////////////////// - // THIS IS WHAT THE USER INTERACTS THROUGH. - ////////////////////////////////////////////// - function fastOnlineSwap(UserOperation calldata userOp) external payable withUserLock(msg.sender) onlyAsControl { - // Calculate the magnitude of the impact of this tx on reputation - uint256 _repMagnitude = gasleft() * tx.gasprice; - - // Track the gas token balance to repay the swapper with - uint256 _gasRefundTracker = address(this).balance - msg.value; - - // Get the userOpHash - bytes32 _userOpHash = IAtlasVerification(ATLAS_VERIFICATION).getUserOperationHash(userOp); - - // Run gas limit checks on the userOp - _validateSwap(userOp); - - // Get and sortSolverOperations - (SolverOperation[] memory _solverOps, uint256 _gasReserved) = _getSolverOps(_userOpHash); - _solverOps = _sortSolverOps(_solverOps); - - // Build dApp operation - DAppOperation memory _dAppOp = _getDAppOp(_userOpHash, userOp.deadline); - - // Atlas call - (bool _success,) = ATLAS.call{ value: msg.value, gas: _metacallGasLimit(_gasReserved, userOp.gas, gasleft()) }( - abi.encodeCall(IAtlas.metacall, (userOp, _solverOps, _dAppOp)) - ); - - // Revert if the metacall failed - neither solvers nor baseline call fulfilled swap intent - if (!_success) revert FLOnlineOuter_FastOnlineSwap_NoFulfillment(); - - // Find out if any of the solvers were successful - _success = _getWinningSolver() != address(0); - - // Update Reputation - _updateSolverReputation(_solverOps, uint128(_repMagnitude)); - - // Handle gas token balance reimbursement (reimbursement from Atlas and the congestion buy ins) - _gasRefundTracker = _processCongestionRake(_gasRefundTracker, _userOpHash, _success); - - // Transfer the appropriate gas tokens - if (_gasRefundTracker > 0) SafeTransferLib.safeTransferETH(msg.sender, _gasRefundTracker); - } - - function _validateSwap(UserOperation calldata userOp) internal { - if (msg.sender != userOp.from) revert FLOnlineOuter_ValidateSwap_InvalidSender(); - - if (userOp.gas <= MAX_SOLVER_GAS * 2) { - revert FLOnlineOuter_ValidateSwap_GasLimitTooLow(); - } - - (SwapIntent memory _swapIntent, BaselineCall memory _baselineCall) = - abi.decode(userOp.data[4:], (SwapIntent, BaselineCall)); - - // Verify that if we're dealing with the native token that the balances add up - if (_swapIntent.tokenUserSells == _NATIVE_TOKEN) { - if (msg.value < userOp.value) revert FLOnlineOuter_ValidateSwap_MsgValueTooLow(); - if (userOp.value < _swapIntent.amountUserSells) revert FLOnlineOuter_ValidateSwap_UserOpValueTooLow(); - if (userOp.value != _baselineCall.value) revert FLOnlineOuter_ValidateSwap_UserOpBaselineValueMismatch(); - } - } - - function _metacallGasLimit( - uint256 cumulativeGasReserved, - uint256 totalGas, - uint256 gasLeft - ) - internal - pure - returns (uint256 metacallGasLimit) - { - // Reduce any unnecessary gas to avoid Atlas's excessive gas bundler penalty - - // About 850k gas extra required to pass Atlas internal checks - cumulativeGasReserved += 850_000; - - // Sets metacallGasLimit to the minimum of {totalGas, gasLeft, cumulativeGasReserved} - metacallGasLimit = totalGas > gasLeft - ? (gasLeft > cumulativeGasReserved ? cumulativeGasReserved : gasLeft) - : (totalGas > cumulativeGasReserved ? cumulativeGasReserved : totalGas); - } - - fallback() external payable { } - - receive() external payable { } -} diff --git a/src/contracts/examples/fastlane-online/FastLaneTypes.sol b/src/contracts/examples/fastlane-online/FastLaneTypes.sol deleted file mode 100644 index 13e4270f8..000000000 --- a/src/contracts/examples/fastlane-online/FastLaneTypes.sol +++ /dev/null @@ -1,21 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// External representation of the swap intent -struct SwapIntent { - address tokenUserBuys; - uint256 minAmountUserBuys; - address tokenUserSells; - uint256 amountUserSells; -} - -struct BaselineCall { - address to; // Address to send the swap if there are no solvers / to get the baseline - bytes data; // Calldata for the baseline swap - uint256 value; // msg.value of the swap (native gas token) -} - -struct Reputation { - uint128 successCost; - uint128 failureCost; -} diff --git a/src/contracts/examples/fastlane-online/IFastLaneOnline.sol b/src/contracts/examples/fastlane-online/IFastLaneOnline.sol deleted file mode 100644 index 412ab2935..000000000 --- a/src/contracts/examples/fastlane-online/IFastLaneOnline.sol +++ /dev/null @@ -1,66 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import { SwapIntent, BaselineCall, Reputation } from "src/contracts/examples/fastlane-online/FastLaneTypes.sol"; - -interface IFastLaneOnline { - // User entrypoint - - function fastOnlineSwap(UserOperation calldata userOp) external payable; - - // Solver functions - - function addSolverOp(UserOperation calldata userOp, SolverOperation calldata solverOp) external payable; - - function refundCongestionBuyIns(SolverOperation calldata solverOp) external; - - // DApp functions - - function setWinningSolver(address winningSolver) external; - - // Other functions - - function makeThogardsWifeHappy() external; - - // View Functions - - function getUserOperationAndHash( - address swapper, - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall, - uint256 deadline, - uint256 gas, - uint256 maxFeePerGas - ) - external - view - returns (UserOperation memory userOp, bytes32 userOpHash); - - function getUserOpHash( - address swapper, - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall, - uint256 deadline, - uint256 gas, - uint256 maxFeePerGas - ) - external - view - returns (bytes32 userOpHash); - - function getUserOperation( - address swapper, - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall, - uint256 deadline, - uint256 gas, - uint256 maxFeePerGas - ) - external - view - returns (UserOperation memory userOp); - - function isUserNonceValid(address owner, uint256 nonce) external view returns (bool valid); -} diff --git a/src/contracts/examples/fastlane-online/OuterHelpers.sol b/src/contracts/examples/fastlane-online/OuterHelpers.sol deleted file mode 100644 index ba2b78615..000000000 --- a/src/contracts/examples/fastlane-online/OuterHelpers.sol +++ /dev/null @@ -1,337 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { LibSort } from "solady/utils/LibSort.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { DAppOperation } from "src/contracts/types/DAppOperation.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; -import "src/contracts/types/EscrowTypes.sol"; - -// Interface Import -import { IAtlasVerification } from "src/contracts/interfaces/IAtlasVerification.sol"; -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; -import { ISimulator } from "src/contracts/interfaces/ISimulator.sol"; - -import { FastLaneOnlineControl } from "src/contracts/examples/fastlane-online/FastLaneControl.sol"; -import { FastLaneOnlineInner } from "src/contracts/examples/fastlane-online/FastLaneOnlineInner.sol"; - -import { SwapIntent, BaselineCall, Reputation } from "src/contracts/examples/fastlane-online/FastLaneTypes.sol"; - -contract OuterHelpers is FastLaneOnlineInner { - // NOTE: Any funds collected in excess of the therapy bills required for the Cardano engineering team - // will go towards buying stealth drones programmed to apply deodorant to coders at solana hackathons. - address public immutable CARDANO_ENGINEER_THERAPY_FUND; - address public immutable PROTOCOL_GUILD_WALLET; - address public immutable SIMULATOR; - - uint256 internal constant _BITS_FOR_INDEX = 16; - - constructor(address atlas, address protocolGuildWallet) FastLaneOnlineInner(atlas) { - CARDANO_ENGINEER_THERAPY_FUND = msg.sender; - PROTOCOL_GUILD_WALLET = protocolGuildWallet; - SIMULATOR = IAtlas(atlas).SIMULATOR(); - } - - ///////////////////////////////////////////////////////// - // CONTROL-LOCAL FUNCTIONS // - // (not delegated) // - ///////////////////////////////////////////////////////// - - function setWinningSolver(address winningSolver) external { - // Only valid time this can be called is during the PostOps phase of a FLOnline metacall. When a user initiates - // that metacall with `fastOnlineSwap()` they are set as the user lock address. So the only time the check below - // will pass is when the caller of this function is the Execution Environment created for the currently active - // user and the FLOnline DAppControl. - - (address expectedCaller,,) = IAtlas(ATLAS).getExecutionEnvironment(_getUserLock(), CONTROL); - if (msg.sender == expectedCaller && _getWinningSolver() == address(0)) { - // Set winning solver in transient storage, to be used in `_updateSolverReputation()` - _setWinningSolver(winningSolver); - } - - // If check above did not pass, gracefully return without setting the winning solver, to not cause the solverOp - // simulation to fail in `addSolverOp()`. - } - - function getUserOperationAndHash( - address swapper, - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall, - uint256 deadline, - uint256 gas, - uint256 maxFeePerGas, - uint256 msgValue - ) - external - view - returns (UserOperation memory userOp, bytes32 userOpHash) - { - userOp = _getUserOperation(swapper, swapIntent, baselineCall, deadline, gas, maxFeePerGas, msgValue); - userOpHash = _getUserOperationHash(userOp); - } - - function getUserOpHash( - address swapper, - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall, - uint256 deadline, - uint256 gas, - uint256 maxFeePerGas, - uint256 msgValue - ) - external - view - returns (bytes32 userOpHash) - { - userOpHash = _getUserOperationHash( - _getUserOperation(swapper, swapIntent, baselineCall, deadline, gas, maxFeePerGas, msgValue) - ); - } - - function getUserOperation( - address swapper, - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall, - uint256 deadline, - uint256 gas, - uint256 maxFeePerGas, - uint256 msgValue - ) - external - view - returns (UserOperation memory userOp) - { - userOp = _getUserOperation(swapper, swapIntent, baselineCall, deadline, gas, maxFeePerGas, msgValue); - } - - function makeThogardsWifeHappy() external onlyAsControl withUserLock(msg.sender) { - if (msg.sender != CARDANO_ENGINEER_THERAPY_FUND) { - revert OuterHelpers_NotMadJustDisappointed(); - } - uint256 _rake = S_rake; - S_rake = 0; - SafeTransferLib.safeTransferETH(CARDANO_ENGINEER_THERAPY_FUND, _rake); - } - - function _simulateSolverOp( - UserOperation calldata userOp, - SolverOperation calldata solverOp - ) - internal - returns (bool valid) - { - DAppOperation memory _dAppOp = _getDAppOp(solverOp.userOpHash, userOp.deadline); - - // NOTE: Valid is false when the solver fails even if postOps is successful - (valid,,) = ISimulator(SIMULATOR).simSolverCall(userOp, solverOp, _dAppOp); - } - - function _getUserOperation( - address swapper, - SwapIntent calldata swapIntent, - BaselineCall calldata baselineCall, - uint256 deadline, - uint256 gas, - uint256 maxFeePerGas, - uint256 msgValue - ) - internal - view - returns (UserOperation memory userOp) - { - userOp = UserOperation({ - from: swapper, - to: ATLAS, - gas: gas, - maxFeePerGas: maxFeePerGas, - nonce: _getNextUserNonce(swapper), - deadline: deadline, - value: msgValue, - dapp: CONTROL, - control: CONTROL, - callConfig: CALL_CONFIG, - sessionKey: address(0), - data: abi.encodeCall(this.swap, (swapIntent, baselineCall)), - signature: new bytes(0) // User must sign - }); - } - - function _getUserOperationHash(UserOperation memory userOp) internal view returns (bytes32 userOpHash) { - userOpHash = IAtlasVerification(ATLAS_VERIFICATION).getUserOperationHash(userOp); - } - - function _getDAppOp(bytes32 userOpHash, uint256 deadline) internal view returns (DAppOperation memory dAppOp) { - dAppOp = DAppOperation({ - from: CONTROL, // signer of the DAppOperation - to: ATLAS, // Atlas address - nonce: 0, // Atlas nonce of the DAppOperation available in the AtlasVerification contract - deadline: deadline, // block.number deadline for the DAppOperation - control: CONTROL, // DAppControl address - bundler: CONTROL, // Signer of the atlas tx (msg.sender) - userOpHash: userOpHash, // keccak256 of userOp.to, userOp.data - callChainHash: bytes32(0), // keccak256 of the solvers' txs - signature: new bytes(0) // NOTE: Control must be registered as signatory of itself, in AtlasVerification. - // Then no signature is required here as control is bundler. - }); - } - - function _processCongestionRake( - uint256 startingBalance, - bytes32 userOpHash, - bool solversSuccessful - ) - internal - returns (uint256 netGasRefund) - { - // Bundler gas rebate from Atlas - uint256 _grossGasRefund = address(this).balance - startingBalance; - // Total congestion buyins for the current userOpHash/metacall - uint256 _congestionBuyIns = S_aggCongestionBuyIn[userOpHash]; - - if (_congestionBuyIns > 0) { - if (solversSuccessful) { - _grossGasRefund += _congestionBuyIns; - } - delete S_aggCongestionBuyIn[userOpHash]; - } - - uint256 _netRake = _grossGasRefund * _CONGESTION_RAKE / _CONGESTION_BASE; - - if (solversSuccessful) { - // If there was a winning solver, increase the FLOnline rake - S_rake += _netRake; - } else { - // NOTE: We do not refund the congestion buyins to the user because we do not want to create a - // scenario in which the user can profit from Solvers failing. We also shouldn't give these to the - // validator for the same reason, nor to the authors of this contract as they should also be credibly - // neutral. - - // So if there is no winning solver, congestion buyins are sent to protocol guild. - SafeTransferLib.safeTransferETH(PROTOCOL_GUILD_WALLET, _congestionBuyIns); - S_rake += _netRake; // rake is only taken on bundler gas rebate from Atlas - } - - // Return the netGasRefund to be sent back to the user - netGasRefund = _grossGasRefund - _netRake; - } - - function _sortSolverOps(SolverOperation[] memory unsortedSolverOps) - internal - pure - returns (SolverOperation[] memory sortedSolverOps) - { - uint256 _length = unsortedSolverOps.length; - if (_length == 0) return unsortedSolverOps; - if (_length == 1 && unsortedSolverOps[0].bidAmount != 0) return unsortedSolverOps; - - uint256[] memory _bidsAndIndices = new uint256[](_length); - uint256 _bidsAndIndicesLastIndex = _length; - uint256 _bidAmount; - - // First encode each solver's bid and their index in the original solverOps array into a single uint256. Build - // an array of these uint256s. - for (uint256 i; i < _length; ++i) { - _bidAmount = unsortedSolverOps[i].bidAmount; - - // skip zero and overflow bid's - if (_bidAmount != 0 && _bidAmount <= type(uint240).max) { - // Set to _length, and decremented before use here to avoid underflow - unchecked { - --_bidsAndIndicesLastIndex; - } - - // Non-zero bids are packed with their original solverOps index. - // The array is filled with non-zero bids from the right. - _bidsAndIndices[_bidsAndIndicesLastIndex] = uint256(_bidAmount << _BITS_FOR_INDEX | uint16(i)); - } - } - - // Create new SolverOps array, large enough to hold all valid bids. - uint256 _sortedSolverOpsLength = _length - _bidsAndIndicesLastIndex; - if (_sortedSolverOpsLength == 0) return sortedSolverOps; // return early if no valid bids - sortedSolverOps = new SolverOperation[](_sortedSolverOpsLength); - - // Reinitialize _bidsAndIndicesLastIndex to the last index of the array - _bidsAndIndicesLastIndex = _length - 1; - - // Sort the array of packed bids and indices in-place, in ascending order of bidAmount. - LibSort.insertionSort(_bidsAndIndices); - - // Finally, iterate through sorted bidsAndIndices array in descending order of bidAmount. - for (uint256 i = _bidsAndIndicesLastIndex;; /* breaks when 0 */ --i) { - // Isolate the bidAmount from the packed uint256 value - _bidAmount = _bidsAndIndices[i] >> _BITS_FOR_INDEX; - - // If we reach the zero bids on the left of array, break as all valid bids already checked. - if (_bidAmount == 0) break; - - // Recover the original index of the SolverOperation - uint256 _index = uint256(uint16(_bidsAndIndices[i])); - - // Add the SolverOperation to the sorted array - sortedSolverOps[_bidsAndIndicesLastIndex - i] = unsortedSolverOps[_index]; - - if (i == 0) break; // break to prevent underflow in next loop - } - - return sortedSolverOps; - } - - function _updateSolverReputation(SolverOperation[] memory solverOps, uint128 magnitude) internal { - uint256 _length = solverOps.length; - address _winningSolver = _getWinningSolver(); - address _solverFrom; - - for (uint256 i; i < _length; i++) { - _solverFrom = solverOps[i].from; - - // winningSolver will be address(0) unless a winning solver fulfilled the swap intent. - if (_solverFrom == _winningSolver) { - S_solverReputations[_solverFrom].successCost += magnitude; - // break out of loop to avoid incrementing failureCost for solvers that did not execute due to being - // after the winning solver in the sorted array. - break; - } else { - S_solverReputations[_solverFrom].failureCost += magnitude; - } - } - - // Clear winning solver, in case `fastOnlineSwap()` is called multiple times in the same tx. - _setWinningSolver(address(0)); - } - - ////////////////////////////////////////////// - ///// GETTERS ////// - ////////////////////////////////////////////// - function _getNextUserNonce(address owner) internal view returns (uint256 nonce) { - nonce = IAtlasVerification(ATLAS_VERIFICATION).getUserNextNonce(owner, false); - } - - function isUserNonceValid(address owner, uint256 nonce) external view returns (bool valid) { - valid = _isUserNonceValid(owner, nonce); - } - - function _isUserNonceValid(address owner, uint256 nonce) internal view returns (bool valid) { - uint248 _wordIndex = uint248(nonce >> 8); - uint8 _bitPos = uint8(nonce); - uint256 _bitmap = IAtlasVerification(ATLAS_VERIFICATION).userNonSequentialNonceTrackers(owner, _wordIndex); - valid = _bitmap & 1 << _bitPos != 1; - } - - ////////////////////////////////////////////// - ///// MODIFIERS ////// - ////////////////////////////////////////////// - modifier onlyAsControl() { - if (address(this) != CONTROL) revert(); - _; - } -} diff --git a/src/contracts/examples/fastlane-online/README.md b/src/contracts/examples/fastlane-online/README.md deleted file mode 100644 index 8bdd1d8a6..000000000 --- a/src/contracts/examples/fastlane-online/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Overview - - - - diff --git a/src/contracts/examples/fastlane-online/SolverGateway.sol b/src/contracts/examples/fastlane-online/SolverGateway.sol deleted file mode 100644 index 681e38e39..000000000 --- a/src/contracts/examples/fastlane-online/SolverGateway.sol +++ /dev/null @@ -1,352 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { DAppOperation } from "src/contracts/types/DAppOperation.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; -import "src/contracts/types/EscrowTypes.sol"; - -// Interface Import -import { IAtlasVerification } from "src/contracts/interfaces/IAtlasVerification.sol"; -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; -import { ISimulator } from "src/contracts/interfaces/ISimulator.sol"; - -import { FastLaneOnlineControl } from "src/contracts/examples/fastlane-online/FastLaneControl.sol"; -import { OuterHelpers } from "src/contracts/examples/fastlane-online/OuterHelpers.sol"; - -import { SwapIntent, BaselineCall, Reputation } from "src/contracts/examples/fastlane-online/FastLaneTypes.sol"; - -contract SolverGateway is OuterHelpers { - uint256 public constant USER_GAS_BUFFER = 350_000; - uint256 public constant MAX_SOLVER_GAS = 500_000; - - uint256 internal constant _SLIPPAGE_BASE = 100; - uint256 internal constant _GLOBAL_MAX_SLIPPAGE = 125; // A lower slippage set by user will override this. - - // bids > sqrt(type(uint256).max / 100) will cause overflow in _calculateBidFactor - uint256 internal constant _MAX_SOLVER_BID = 34_028_236_692_093_846_346_337_460_743_176_821_145; - - constructor(address atlas, address protocolGuildWallet) OuterHelpers(atlas, protocolGuildWallet) { } - - function getSolverGasLimit() public pure override returns (uint32) { - return uint32(MAX_SOLVER_GAS); - } - - ///////////////////////////////////////////////////////// - // CONTROL-LOCAL FUNCTIONS // - // (not delegated) // - ///////////////////////////////////////////////////////// - - ///////////////////////////////////////////////////////// - // EXTERNAL INTERFACE FUNCS // - ///////////////////////////////////////////////////////// - // FOR SOLVERS // - ///////////////////////////////////////////////////////// - - // Note: this function involves calling the simulator, which has a few requirements which must be met for this - // function to succeed: - // - There must be at least 1 million (500k + 500k) gas left by the time `_executeSolverOperation()` is called. So - // the make the gas limit of this function call high enough to allow for that. 1.5 million gas should be enough. - function addSolverOp( - UserOperation calldata userOp, - SolverOperation calldata solverOp - ) - external - payable - onlyAsControl - withUserLock(solverOp.from) - { - if (msg.sender != solverOp.from) revert SolverGateway_AddSolverOp_SolverMustBeSender(); - if (solverOp.bidAmount > _MAX_SOLVER_BID) revert SolverGateway_AddSolverOp_BidTooHigh(); - - if (S_solverOpHashes[solverOp.userOpHash].length == 0) { - // First solverOp of each userOp deploys the user's Execution Environment - IAtlas(ATLAS).createExecutionEnvironment(userOp.from, address(this)); - } - - // Simulate the SolverOp and make sure it's valid - if (!_simulateSolverOp(userOp, solverOp)) revert SolverGateway_AddSolverOp_SimulationFail(); - - bytes32 _solverOpHash = keccak256(abi.encode(solverOp)); - - (bool _pushAsNew, bool _replaceExisting, uint256 _replacedIndex) = _evaluateForInclusion(userOp, solverOp); - - if (_pushAsNew) { - _pushSolverOp(solverOp.userOpHash, _solverOpHash); - } else if (_replaceExisting) { - _replaceSolverOp(solverOp.userOpHash, _solverOpHash, _replacedIndex); - } else { - // revert if pushAsNew = false and replaceExisting = false - revert SolverGateway_AddSolverOp_ScoreTooLow(); - } - - // Store the op - S_solverOpCache[_solverOpHash] = solverOp; - } - - function refundCongestionBuyIns(SolverOperation calldata solverOp) - external - withUserLock(solverOp.from) - onlyAsControl - { - // NOTE: Anyone can call this on behalf of the solver - // NOTE: the solverOp deadline cannot be before the userOp deadline, therefore if the - // solverOp deadline is passed then we know the userOp deadline is passed. - if (solverOp.deadline >= block.number) { - revert SolverGateway_RefundCongestionBuyIns_DeadlineNotPassed(); - } - - bytes32 _solverOpHash = keccak256(abi.encode(solverOp)); - - uint256 _congestionBuyIn = S_congestionBuyIn[_solverOpHash]; - uint256 _aggCongestionBuyIn = S_aggCongestionBuyIn[solverOp.userOpHash]; - - // NOTE: On successful execution, the _aggCongestionBuyIn is set to zero - // but the individual _congestionBuyIns are not, so verify both. - if (_congestionBuyIn > 0 && _aggCongestionBuyIn >= _congestionBuyIn) { - delete S_congestionBuyIn[_solverOpHash]; - S_aggCongestionBuyIn[solverOp.userOpHash] -= _congestionBuyIn; - - SafeTransferLib.safeTransferETH(solverOp.from, _congestionBuyIn); - } - } - - ///////////////////////////////////////////////////////// - // EXTERNAL INTERFACE FUNCS // - // FOR DAPP CONTROL // - ///////////////////////////////////////////////////////// - function getBidAmount(bytes32 solverOpHash) external view returns (uint256 bidAmount) { - return S_solverOpCache[solverOpHash].bidAmount; - } - - ///////////////////////////////////////////////////////// - // INTERNAL FUNCS // - ///////////////////////////////////////////////////////// - function _pushSolverOp(bytes32 userOpHash, bytes32 solverOpHash) internal { - // Push to array - S_solverOpHashes[userOpHash].push(solverOpHash); - - // Accounting - if (msg.value > 0) { - S_aggCongestionBuyIn[userOpHash] += msg.value; - S_congestionBuyIn[solverOpHash] = msg.value; - } - } - - function _replaceSolverOp(bytes32 userOpHash, bytes32 solverOpHash, uint256 replacedIndex) internal { - // Handle the removed solverOp - bytes32 _replacedHash = S_solverOpHashes[userOpHash][replacedIndex]; - uint256 _replacedCongestionBuyIn = S_congestionBuyIn[_replacedHash]; - - // Handle xfer back - if (_replacedCongestionBuyIn > 0) { - // Accounting (remove balance before xfer) - delete S_congestionBuyIn[_replacedHash]; - - SolverOperation memory _replacedSolverOp = S_solverOpCache[solverOpHash]; - - if (_replacedSolverOp.from.code.length == 0) { - // Transfer their congestion buyin back - SafeTransferLib.safeTransferETH(_replacedSolverOp.from, _replacedCongestionBuyIn); - } - } - - // Accounting - if (_replacedCongestionBuyIn > msg.value) { - S_aggCongestionBuyIn[userOpHash] -= (_replacedCongestionBuyIn - msg.value); - } else if (_replacedCongestionBuyIn < msg.value) { - S_aggCongestionBuyIn[userOpHash] += (msg.value - _replacedCongestionBuyIn); - } // if they're equal, do nothing. - - if (msg.value > 0) { - S_congestionBuyIn[solverOpHash] = msg.value; - } - - S_solverOpHashes[userOpHash][replacedIndex] = solverOpHash; - } - - function _getSolverOps(bytes32 userOpHash) - internal - view - returns (SolverOperation[] memory solverOps, uint256 cumulativeGasReserved) - { - uint256 _totalSolvers = S_solverOpHashes[userOpHash].length; - - solverOps = new SolverOperation[](_totalSolvers); - - for (uint256 _j; _j < _totalSolvers; _j++) { - bytes32 _solverOpHash = S_solverOpHashes[userOpHash][_j]; - SolverOperation memory _solverOp = S_solverOpCache[_solverOpHash]; - solverOps[_j] = _solverOp; - cumulativeGasReserved += _solverOp.gas; - } - } - - function _evaluateForInclusion( - UserOperation calldata userOp, - SolverOperation calldata solverOp - ) - internal - view - returns (bool pushAsNew, bool replaceExisting, uint256 /* replacedIndex */ ) - { - (SolverOperation[] memory _solverOps, uint256 _cumulativeGasReserved) = _getSolverOps(solverOp.userOpHash); - - if (_solverOps.length == 0) { - return (true, false, 0); - } - - (SwapIntent memory swapIntent,) = abi.decode(userOp.data[4:], (SwapIntent, BaselineCall)); - - (uint256 _cumulativeScore, uint256 _replacedIndex) = - _getCumulativeScores(swapIntent, _solverOps, userOp.gas, userOp.maxFeePerGas); - - uint256 _score = _getWeightedScoreNewSolver( - userOp.gas, userOp.maxFeePerGas, swapIntent.minAmountUserBuys, _solverOps.length, solverOp - ); - - // Check can be grokked more easily in the following format: - // solverOpScore _cumulativeScore (unweighted) - // if -------------- > ------------------------------ * 2 - // solverOpGas totalGas - - if (_score * userOp.gas > _cumulativeScore * solverOp.gas * 2) { - if (_cumulativeGasReserved + USER_GAS_BUFFER + solverOp.gas < userOp.gas) { - // If enough gas in metacall limit to fit new solverOp, add as new. - return (true, false, 0); - } else { - // Otherwise replace the solverOp with lowest score. - return (false, true, _replacedIndex); - } - } - // If the new solverOp's score/gas ratio is too low, don't include it at all. This will result in a - // SolverGateway_AddSolverOp_ScoreTooLow error in `addSolverOp()`. - return (false, false, 0); - } - - function _getCumulativeScores( - SwapIntent memory swapIntent, - SolverOperation[] memory solverOps, - uint256 gas, - uint256 maxFeePerGas - ) - internal - view - returns (uint256 cumulativeScore, uint256 replacedIndex) - { - uint256 _lowestScore; - uint256 _length = solverOps.length; - for (uint256 _i; _i < _length; _i++) { - SolverOperation memory _solverOp = solverOps[_i]; - - uint256 _score = _getWeightedScore(gas, maxFeePerGas, swapIntent.minAmountUserBuys, _length, _solverOp); - - if (_i == 0 || _score < _lowestScore) { - replacedIndex = _i; - _lowestScore = _score; - } - - cumulativeScore += _score; - } - } - - function _getWeightedScore( - uint256 totalGas, - uint256 maxFeePerGas, - uint256 minAmountUserBuys, - uint256 solverCount, - SolverOperation memory solverOp - ) - internal - view - returns (uint256 score) - { - bytes32 _solverOpHash = keccak256(abi.encode(solverOp)); - uint256 _congestionBuyIn = S_congestionBuyIn[_solverOpHash]; - uint256 _bidFactor = _calculateBidFactor(solverOp.bidAmount, minAmountUserBuys); - - score = _calculateWeightedScore({ - totalGas: totalGas, - solverOpGas: solverOp.gas, - maxFeePerGas: maxFeePerGas, - congestionBuyIn: _congestionBuyIn, - solverCount: solverCount, - bidFactor: _bidFactor, - rep: S_solverReputations[solverOp.from] - }); - } - - function _getWeightedScoreNewSolver( - uint256 totalGas, - uint256 maxFeePerGas, - uint256 minAmountUserBuys, - uint256 solverCount, - SolverOperation calldata solverOp - ) - internal - view - returns (uint256 score) - { - uint256 _bidFactor = _calculateBidFactor(solverOp.bidAmount, minAmountUserBuys); - - score = _calculateWeightedScore({ - totalGas: totalGas, - solverOpGas: solverOp.gas, - maxFeePerGas: maxFeePerGas, - congestionBuyIn: msg.value, - solverCount: solverCount, - bidFactor: _bidFactor, - rep: S_solverReputations[solverOp.from] - }); - } - - function _calculateWeightedScore( - uint256 totalGas, - uint256 solverOpGas, - uint256 maxFeePerGas, - uint256 congestionBuyIn, - uint256 solverCount, - uint256 bidFactor, - Reputation memory rep - ) - internal - pure - returns (uint256 score) - { - score = ( - (congestionBuyIn + (maxFeePerGas * totalGas)) // A solver typically has to pay maxFeePerGas * gas as a - // requirement for winning. - * totalGas / (totalGas + solverOpGas) // double count gas by doing this even in unweighted score (there's - // value in packing more solutions) - * (uint256(rep.successCost) + (maxFeePerGas * totalGas)) - / (uint256(rep.failureCost) + (maxFeePerGas * totalGas * (solverCount + 1))) // as solverCount increases, - // the dilution of thin auction history increases. - * bidFactor / solverOpGas - ); - } - - function _calculateBidFactor( - uint256 bidAmount, - uint256 minAmountUserBuys - ) - internal - pure - returns (uint256 bidFactor) - { - // To avoid truncating to zero, check and return the minimum slippage - if (bidAmount < minAmountUserBuys + 1) return _SLIPPAGE_BASE; - - // NOTE: bidAmount is checked to be < _MAX_SOLVER_BID in addSolverOp to prevent overflow here - bidFactor = (bidAmount ** 2) * _SLIPPAGE_BASE / (minAmountUserBuys + 1) ** 2; - if (bidFactor > _GLOBAL_MAX_SLIPPAGE) bidFactor = _GLOBAL_MAX_SLIPPAGE; - } -} diff --git a/src/contracts/examples/gas-filler/Filler.sol b/src/contracts/examples/gas-filler/Filler.sol deleted file mode 100644 index a0e9ee643..000000000 --- a/src/contracts/examples/gas-filler/Filler.sol +++ /dev/null @@ -1,290 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; - -// Atlas Base Imports -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; - -import { SafetyBits } from "src/contracts/libraries/SafetyBits.sol"; - -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Atlas DApp-Control Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; - -interface IERC20 { - function allowance(address owner, address spender) external view returns (uint256); - function approve(address spender, uint256 amount) external returns (bool); - function balanceOf(address account) external view returns (uint256); - function transfer(address to, uint256 amount) external returns (bool); - function transferFrom(address from, address to, uint256 amount) external returns (bool); -} - -contract Filler is DAppControl { - uint256 public constant CONTROL_GAS_USAGE = 250_000; - - struct AccessTuple { - address accessAddress; - bytes32[] accessStorageKeys; - } - - // TODO: Need to use assembly to pack and unpack this correctly. Surely there's a lib somewhere? - struct ApprovalTx { - // address from; // technically txs don't have a from, may need to remove from sig - uint64 txType; - uint256 chainID; - uint64 nonce; - uint256 gasPrice; // legacy tx gasprice - uint256 gasFeeCap; // 1559 maxFeePerGas - uint256 gasTipCap; // 1559 maxPriorityFeePerGas - uint64 gasLimit; // aka gas - address to; - uint256 value; // aka amount - bytes data; - AccessTuple[] accessList; - uint8 v; - bytes32 r; - bytes32 s; - } - - // NOTE: this is accessed on the control contract itself, not the EE - uint256 public owed; - uint256 public prepaid; - - // NOTE: this is accessed on the control contract itself, not the EE? - address public userLock; - bytes32 public hashLock; - - constructor( - address _atlas, - address _wrappedGasToken - ) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: false, - trackPreOpsReturnData: false, - trackUserReturnData: true, - delegateUser: true, - requirePreSolver: true, - requirePostSolver: true, - requirePostOps: false, - zeroSolvers: false, - reuseUserOp: false, - userAuctioneer: false, - solverAuctioneer: false, - unknownAuctioneer: false, - verifyCallChainHash: true, - forwardReturnData: true, - requireFulfillment: true, - trustedOpHash: false, - invertBidValue: true, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { } - - // This occurs after a Solver has successfully paid their bid, which is - // held in ExecutionEnvironment. - function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata) internal override { - // NOTE: gas value xferred to user in postSolverCall - // Pay the solver (since auction is reversed) - // Address Pointer = winning solver. - - // revert for ERC20 tokens - if (bidToken != address(0)) revert("ERC20 bids not supported"); - - SafeTransferLib.safeTransferETH(_user(), bidAmount); - } - - function _preSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override { - address solverTo = solverOp.solver; - if (solverTo == address(this) || solverTo == _control() || solverTo == ATLAS) { - revert(); - } - - (address approvalToken, uint256 maxTokenAmount,) = abi.decode(returnData, (address, uint256, uint256)); - - if (solverOp.bidAmount > maxTokenAmount) revert(); - if (solverOp.bidToken != approvalToken) revert(); - - _transferDAppERC20(approvalToken, solverOp.solver, solverOp.bidAmount); - - return; // success - } - - function _postSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override { - (, uint256 maxTokenAmount, uint256 gasNeeded) = abi.decode(returnData, (address, uint256, uint256)); - - require(address(this).balance >= gasNeeded, "ERR - EXISTING GAS BALANCE"); - - bytes memory data = abi.encodeCall(this.postOpBalancing, maxTokenAmount - solverOp.bidAmount); - - (bool success,) = _control().call(_forward(data)); - require(success, "HITTING THIS = JOB OFFER"); - - return; // success - } - - ///////////////// GETTERS & HELPERS // ////////////////// - - function getBidFormat(UserOperation calldata userOp) public view override returns (address bidToken) { - // This is a helper function called by solvers - // so that they can get the proper format for - // submitting their bids to the hook. - (, ApprovalTx memory approvalTx) = _decodeRawData(userOp.data[4:]); - return approvalTx.to; - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } - - ///////////////////// DAPP STUFF /////////////////////// - - function postOpBalancing(uint256 prepaidAmount) external { - require(msg.sender == _activeEnvironment(), "ERR - INVALID SENDER"); - require(address(this) == _control(), "ERR - INVALID CONTROL"); - require(_depth() == 2, "ERR - INVALID DEPTH"); - - prepaid = prepaidAmount; - } - - // FUN AND TOTALLY UNNECESSARY MIND WORM - // (BUT IT HELPS WITH MENTALLY GROKKING THE FLOW) - function approve(bytes calldata data) external returns (bytes memory) { - // CASE: Base call - if (msg.sender == ATLAS) { - require(address(this) != _control(), "ERR - NOT DELEGATED"); - return _innerApprove(data); - } - - // CASE: Nested call from Atlas EE - if (msg.sender == _activeEnvironment()) { - require(address(this) == _control(), "ERR - INVALID CONTROL"); - return _outerApprove(data); - } - - // CASE: Non-Atlas external call - require(address(this) == _control(), "ERR - INVALID CONTROL"); - _externalApprove(data); - return new bytes(0); - } - - function _innerApprove(bytes calldata data) internal returns (bytes memory) { - (, ApprovalTx memory approvalTx) = _decodeRawData(data); - - address approvalToken = approvalTx.to; - - require(IERC20(approvalToken).balanceOf(address(this)) == 0, "ERR - EXISTING ERC20 BALANCE"); - require(address(this).balance == 0, "ERR - EXISTING GAS BALANCE"); - - // TODO: use assembly (current impl is a lazy way to grab the approval tx data) - bytes memory mData = abi.encodeCall(this.approve, bytes.concat(approvalTx.data, data)); - - (bool success, bytes memory returnData) = _control().call(_forward(mData)); - // NOTE: approvalTx.data includes func selector - - require(success, "ERR - REJECTED"); - - return abi.decode(returnData, (bytes)); - } - - function _outerApprove(bytes calldata data) internal returns (bytes memory) { - (address spender, uint256 amount, uint256 gasNeeded, ApprovalTx memory approvalTx) = - abi.decode(data[4:], (address, uint256, uint256, ApprovalTx)); - - address user = _user(); - address approvalToken = approvalTx.to; - - require(user.code.length == 0, "ERR - NOT FOR SMART ACCOUNTS"); // NOTE shouldn't be necessary b/c sig check - // but might change sig check in future versions to support smart accounts, in which case revisit safety - // checks at this stage. - - require(spender == address(this), "ERR - INVALID SPENDER"); - require(IERC20(approvalToken).allowance(user, address(this)) == 0, "ERR - EXISTING APPROVAL"); - require(IERC20(approvalToken).allowance(address(this), ATLAS) >= amount, "ERR - TOKEN UNAPPROVED"); - require(amount <= IERC20(approvalToken).balanceOf(address(this)), "ERR - POOL BALANCE TOO LOW"); - require(amount <= IERC20(approvalToken).balanceOf(user), "ERR - USER BALANCE TOO LOW"); - require(userLock == address(0), "ERR - USER ALREADY LOCKED"); - require(hashLock == bytes32(0), "ERR - HASH LOCK ALREADY LOCKED"); - require(owed == 0, "ERR - BALANCE OUTSTANDING"); - require(prepaid == 0, "ERR - USER ALREADY OWES"); - - // TODO: Gas calcs (tx type specific) to ensure that allowance gas cost is covered by amount - - hashLock = _getApprovalTxHash(approvalTx); - userLock = user; - owed = amount; - - // IERC20(approvalToken).transfer(msg.sender, amount); - // NOTE: We handle the token xfers with dapp-side permit69 - - return abi.encode(approvalToken, amount, gasNeeded); - } - - function _externalApprove(bytes calldata data) internal { - // data = UserOperation.data - require(bytes4(data) == this.approve.selector, "ERR - INVALID FUNC"); - - (, ApprovalTx memory approvalTx) = abi.decode(data[:4], (uint256, ApprovalTx)); - - // Check locks - bytes32 approvalHash = _getApprovalTxHash(approvalTx); - require(approvalHash == hashLock, "ERR - INVALID HASH"); - require(userLock != address(0), "ERR - USER UNLOCKED"); - - // Verify balances - address approvalToken = approvalTx.to; - - address _userLock = userLock; - uint256 _owed = owed; - uint256 _prepaid = prepaid; - - require(_owed >= _prepaid, "ERR - PREPAID TOO HIGH"); // should get caught as overflow below - - uint256 allowance = IERC20(approvalToken).allowance(_userLock, address(this)); - require(allowance >= _owed - _prepaid, "ERR - ALLOWANCE TOO LOW"); - - // use up the entire allowance - IERC20(approvalToken).transferFrom(_userLock, address(this), allowance); - - // transfer back the prepaid amount - IERC20(approvalToken).transfer(_userLock, _prepaid); - - // Clear the locks - delete userLock; - delete hashLock; - delete owed; - delete prepaid; - } - - function _decodeRawData(bytes calldata data) - internal - pure - returns (uint256 gasNeeded, ApprovalTx memory approvalTx) - { - // BELOW HERE IS WRONG - TODO: COMPLETE - (gasNeeded, approvalTx) = abi.decode(data, (uint256, ApprovalTx)); - address signer = _user(); - // TODO: NEED TO SIG VERIFY JUST approvalTx - // ABOVE HERE IS WRONG - TODO: COMPLETE - - require(signer == _user(), "ERR - INVALID SIGNER"); - } - - function _getApprovalTxHash(ApprovalTx memory approvalTx) internal returns (bytes32) { - // TODO: this is wrong - return keccak256(abi.encode(approvalTx)); - } -} diff --git a/src/contracts/examples/generalized-backrun/GeneralizedBackrunUserBundler.sol b/src/contracts/examples/generalized-backrun/GeneralizedBackrunUserBundler.sol deleted file mode 100644 index 6ae07c1d3..000000000 --- a/src/contracts/examples/generalized-backrun/GeneralizedBackrunUserBundler.sol +++ /dev/null @@ -1,364 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { DAppOperation } from "src/contracts/types/DAppOperation.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Interface Import -import { IAtlasVerification } from "src/contracts/interfaces/IAtlasVerification.sol"; -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; - -struct Approval { - address token; - address spender; - uint256 amount; -} - -struct Beneficiary { - address owner; - uint256 percentage; // out of 100 -} -// NOTE user gets remainder - -interface IGeneralizedBackrunProxy { - function getUser() external view returns (address); -} - -contract GeneralizedBackrunUserBundler is DAppControl { - address private _userLock = address(1); // TODO: Convert to transient storage - - uint256 private constant _FEE_BASE = 100; - - // USER TOKEN AMOUNT - mapping(address => mapping(address => uint256)) internal s_deposits; - - // SolverOpHash SolverOperation - mapping(bytes32 => SolverOperation) public S_solverOpCache; - - // UserOpHash SolverOpHash[] - mapping(bytes32 => bytes32[]) public S_solverOpHashes; - - constructor(address _atlas) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: false, - trackPreOpsReturnData: false, - trackUserReturnData: true, - delegateUser: true, - requirePreSolver: false, - requirePostSolver: false, - requirePostOps: false, - zeroSolvers: true, - reuseUserOp: true, - userAuctioneer: true, - solverAuctioneer: false, - unknownAuctioneer: false, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: false, - trustedOpHash: true, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: true - }) - ) - { } - - // ---------------------------------------------------- // - // Custom // - // ---------------------------------------------------- // - - ///////////////////////////////////////////////////////// - // CONTROL FUNCTIONS // - // (not delegated) // - ///////////////////////////////////////////////////////// - - modifier onlyAsControl() { - if (address(this) != CONTROL) revert(); - _; - } - - modifier withUserLock(address user) { - if (_userLock != address(1)) revert(); - _userLock = user; - _; - _userLock = address(1); - } - - modifier onlyWhenUnlocked() { - if (_userLock != address(1)) revert(); - _; - } - - function getUser() external view onlyAsControl returns (address) { - address _user = _userLock; - if (_user == address(1)) revert(); - return _user; - } - - function addSolverOp(SolverOperation calldata solverOp) external onlyAsControl { - /* - // SolverOpHash SolverOperation - mapping(bytes32 => SolverOperation) public S_solverOpCache; - - // UserOpHash SolverOpHash[] - mapping(bytes32 => bytes32[]) public S_solverOpHashes; - */ - if (msg.sender != solverOp.from) revert(); - - bytes32 _solverOpHash = keccak256(abi.encode(solverOp)); - - S_solverOpCache[_solverOpHash] = solverOp; - S_solverOpHashes[solverOp.userOpHash].push(_solverOpHash); - } - - // Entrypoint function for usage with permit / permit2 / bridges / whatever - function bundledProxyCall( - UserOperation calldata userOp, - address transferHelper, - bytes calldata transferData, - bytes32[] calldata solverOpHashes - ) - external - payable - withUserLock(userOp.from) - onlyAsControl - { - // Decode the token information - (Approval[] memory _approvals,,,,) = - abi.decode(userOp.data[4:], (Approval[], address[], Beneficiary[], address, bytes)); - - // Process token transfers if necessary. If transferHelper == address(0), skip. - if (transferHelper != address(0)) { - (bool _success, bytes memory _data) = transferHelper.call(transferData); - if (!_success) { - assembly { - revert(add(_data, 32), mload(_data)) - } - } - - // Get the execution environment address - (address _environment,,) = IAtlas(ATLAS).getExecutionEnvironment(userOp.from, CONTROL); - - for (uint256 i; i < _approvals.length; i++) { - uint256 _balance = IERC20(_approvals[i].token).balanceOf(address(this)); - if (_balance != 0) { - IERC20(_approvals[i].token).transfer(_environment, _balance); - } - } - } - - uint256 _bundlerRefundTracker = address(this).balance - msg.value; - - bytes32 _userOpHash = IAtlasVerification(ATLAS_VERIFICATION).getUserOperationHash(userOp); - - DAppOperation memory _dAppOp = DAppOperation({ - from: address(this), // signer of the DAppOperation - to: ATLAS, // Atlas address - nonce: 0, // Atlas nonce of the DAppOperation available in the AtlasVerification contract - deadline: userOp.deadline, // block.number deadline for the DAppOperation - control: address(this), // DAppControl address - bundler: address(this), // Signer of the atlas tx (msg.sender) - userOpHash: _userOpHash, // keccak256 of userOp.to, userOp.data - callChainHash: bytes32(0), // keccak256 of the solvers' txs - signature: new bytes(0) // DAppOperation signed by DAppOperation.from - }); - - // TODO: Add in the solverOp grabber - SolverOperation[] memory _solverOps = _getSolverOps(solverOpHashes); - - (bool _success, bytes memory _data) = - ATLAS.call{ value: msg.value }(abi.encodeCall(IAtlas.metacall, (userOp, _solverOps, _dAppOp))); - if (!_success) { - assembly { - revert(add(_data, 32), mload(_data)) - } - } - - if (address(this).balance > _bundlerRefundTracker) { - SafeTransferLib.safeTransferETH(msg.sender, address(this).balance - _bundlerRefundTracker); - } - - // TODO: add bundler subsidy capabilities for apps. - } - - function _getSolverOps(bytes32[] calldata solverOpHashes) - internal - view - returns (SolverOperation[] memory solverOps) - { - solverOps = new SolverOperation[](solverOpHashes.length); - - uint256 _j; - for (uint256 i; i < solverOpHashes.length; i++) { - SolverOperation memory _solverOp = S_solverOpCache[solverOpHashes[i]]; - if (_solverOp.from != address(0)) { - solverOps[_j++] = _solverOp; - } - } - } - - ///////////////////////////////////////////////////////// - // EXECUTION ENVIRONMENT FUNCTIONS // - // (not delegated) // - ///////////////////////////////////////////////////////// - - // NOTE: this is delegatecalled - function proxyCall( - Approval[] calldata approvals, - address[] calldata receivables, - Beneficiary[] calldata beneficiaries, - address innerTarget, - bytes calldata innerData - ) - external - payable - onlyAtlasEnvironment - returns (Beneficiary[] memory) - { - bool _isProxied; - - // CASE: Bundled (Force all bundlers to go through bundler contract (this one)) - if (_bundler() == CONTROL) { - if (IGeneralizedBackrunProxy(CONTROL).getUser() != _user()) revert(); - _isProxied = true; - - // CASE: Direct - } else if (_bundler() != _user()) { - // revert if bundler isn't CONTROL or _user() - revert(); - } - - return _proxyCall(approvals, receivables, beneficiaries, innerTarget, innerData, _isProxied); - } - - function _proxyCall( - Approval[] calldata approvals, - address[] calldata receivables, - Beneficiary[] calldata beneficiaries, - address innerTarget, - bytes calldata innerData, - bool isProxied - ) - internal - returns (Beneficiary[] memory) - { - address _recipient = _user(); - - // Handle approvals - for (uint256 i; i < approvals.length; i++) { - Approval calldata approval = approvals[i]; - - // CASE: Proxied - user should have signed a permit or permit2 - if (isProxied) { - uint256 _currentBalance = IERC20(approval.token).balanceOf(address(this)); - if (approval.amount > _currentBalance) { - _transferUserERC20(approval.token, address(this), approval.amount - _currentBalance); - } - } else { - // Transfer the User's token to the EE: - _transferUserERC20(approval.token, address(this), approval.amount); - } - - // Have the EE approve the User's target: - IERC20(approval.token).approve(approval.spender, approval.amount); - } - - // Do the actual call - (bool _success, bytes memory _data) = innerTarget.call{ value: msg.value }(innerData); - // Bubble up the revert message (note there's not really a reason to do this, - // we'll replace with a custom error soon. - if (!_success) { - assembly { - revert(add(_data, 32), mload(_data)) - } - } - - // Reset approvals for the EE - for (uint256 i; i < approvals.length; i++) { - Approval calldata approval = approvals[i]; - - // Remove the EE's approvals - IERC20(approval.token).approve(approval.spender, 0); - - uint256 _balance = IERC20(approval.token).balanceOf(address(this)); - - // Transfer any leftover tokens back to the User - if (_balance != 0) { - IERC20(approval.token).transfer(_recipient, _balance); - } - } - - // Return the receivable tokens to the user - for (uint256 i; i < receivables.length; i++) { - address _receivable = receivables[i]; - uint256 _balance = IERC20(_receivable).balanceOf(address(this)); - - // Transfer the EE's tokens (note that this will revert if balance is insufficient) - if (_balance != 0) { - IERC20(_receivable).transfer(_recipient, _balance); - } - } - - // Forward any value that accrued to the bundler (either user or contract) - don't share w/ solvers - uint256 _balance = address(this).balance; - if (_balance > 0) { - SafeTransferLib.safeTransferETH(_bundler(), _balance); - } - return beneficiaries; - } - - // ---------------------------------------------------- // - // Atlas hooks // - // ---------------------------------------------------- // - - function _allocateValueCall(address bidToken, uint256, bytes calldata returnData) internal override { - // NOTE: The _user() receives any remaining balance after the other beneficiaries are paid. - Beneficiary[] memory _beneficiaries = abi.decode(returnData, (Beneficiary[])); - - uint256 _unallocatedPercent = _FEE_BASE; - uint256 _balance = address(this).balance; - - // Return the receivable tokens to the user - for (uint256 i; i < _beneficiaries.length; i++) { - uint256 _percentage = _beneficiaries[i].percentage; - if (_percentage < _unallocatedPercent) { - _unallocatedPercent -= _percentage; - SafeTransferLib.safeTransferETH(_beneficiaries[i].owner, _balance * _percentage / _FEE_BASE); - } else { - SafeTransferLib.safeTransferETH(_beneficiaries[i].owner, address(this).balance); - } - } - - // Transfer the remaining value to the user - if (_unallocatedPercent != 0) { - SafeTransferLib.safeTransferETH(_user(), address(this).balance); - } - } - - // ---------------------------------------------------- // - // Getters and helpers // - // ---------------------------------------------------- // - - function getBidFormat(UserOperation calldata userOp) public pure override returns (address bidToken) { - return address(0); - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } -} diff --git a/src/contracts/examples/intents-example/StateIntent.sol b/src/contracts/examples/intents-example/StateIntent.sol deleted file mode 100644 index 37a2e24b8..000000000 --- a/src/contracts/examples/intents-example/StateIntent.sol +++ /dev/null @@ -1,62 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Atlas Base Imports -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; - -import { SafetyBits } from "src/contracts/libraries/SafetyBits.sol"; - -import "src/contracts/types/LockTypes.sol"; - -// Atlas DApp-Control Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; - -import "forge-std/Test.sol"; - -// under construction - -enum TimingPath { - EarliestBlock, - LatestBlock, - ExactBlock, - EarliestTime, - LatestTime, - ExactTime -} - -struct Timing { - TimingPath path; - uint256 value; -} - -enum StatePath { - PriorAbsolute, - PostAbsolute, - PositiveDelta, - NegativeDelta -} - -struct StateExpression { - StatePath path; - uint8 offset; - uint8 size; - bytes32 slot; - uint256 value; -} - -struct StatePreference { - address src; - StateExpression[] exp; -} - -struct Payment { - address token; - uint256 amount; -} - -struct Intent { - StatePreference[] preferences; - Timing time; - Payment pmt; - address from; -} diff --git a/src/contracts/examples/intents-example/SwapIntentDAppControl.sol b/src/contracts/examples/intents-example/SwapIntentDAppControl.sol deleted file mode 100644 index 8aabc1b1c..000000000 --- a/src/contracts/examples/intents-example/SwapIntentDAppControl.sol +++ /dev/null @@ -1,197 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -struct Condition { - address antecedent; - bytes context; -} - -// External representation of the swap intent -struct SwapIntent { - address tokenUserBuys; - uint256 amountUserBuys; - address tokenUserSells; - uint256 amountUserSells; - address auctionBaseCurrency; - Condition[] conditions; // Optional -} - -// Internal representation of the swap intent -struct SwapData { - address tokenUserBuys; - uint256 amountUserBuys; - address tokenUserSells; - uint256 amountUserSells; - address auctionBaseCurrency; -} - -contract SwapIntentDAppControl is DAppControl { - uint256 public constant USER_CONDITION_GAS_LIMIT = 20_000; - uint256 public constant MAX_USER_CONDITIONS = 5; - - constructor(address _atlas) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: false, - trackPreOpsReturnData: false, - trackUserReturnData: true, - delegateUser: true, - requirePreSolver: true, - requirePostSolver: true, - requirePostOps: false, - zeroSolvers: false, - reuseUserOp: true, - userAuctioneer: false, - solverAuctioneer: false, - unknownAuctioneer: false, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: true, - trustedOpHash: true, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { } - - // ---------------------------------------------------- // - // Custom // - // ---------------------------------------------------- // - - /* - * @notice This is the user operation target function - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev selector = 0x98434997 - * @dev It checks that the user has approved Atlas to spend the tokens they are selling and the conditions are met - * @param swapIntent The SwapIntent struct - * @return swapData The SwapData struct - */ - function swap(SwapIntent calldata swapIntent) external payable returns (SwapData memory) { - require(msg.sender == ATLAS, "SwapIntentDAppControl: InvalidSender"); - require(address(this) != CONTROL, "SwapIntentDAppControl: MustBeDelegated"); - require(swapIntent.tokenUserSells != swapIntent.auctionBaseCurrency, "SwapIntentDAppControl: SellIsSurplus"); - - address user = _user(); - - require( - _availableFundsERC20(swapIntent.tokenUserSells, user, swapIntent.amountUserSells, ExecutionPhase.PreSolver), - "SwapIntentDAppControl: SellFundsUnavailable" - ); - - uint256 conditionsLength = swapIntent.conditions.length; - if (conditionsLength > 0) { - require(conditionsLength <= MAX_USER_CONDITIONS, "SwapIntentDAppControl: TooManyConditions"); - - bool valid; - bytes memory conditionData; - - for (uint256 i; i < conditionsLength; ++i) { - (valid, conditionData) = swapIntent.conditions[i].antecedent.staticcall{ gas: USER_CONDITION_GAS_LIMIT }( - swapIntent.conditions[i].context - ); - require(valid && abi.decode(conditionData, (bool)), "SwapIntentDAppControl: ConditionUnsound"); - } - } - - return SwapData({ - tokenUserBuys: swapIntent.tokenUserBuys, - amountUserBuys: swapIntent.amountUserBuys, - tokenUserSells: swapIntent.tokenUserSells, - amountUserSells: swapIntent.amountUserSells, - auctionBaseCurrency: swapIntent.auctionBaseCurrency - }); - } - - // ---------------------------------------------------- // - // Atlas hooks // - // ---------------------------------------------------- // - - /* - * @notice This function is called before a solver operation executes - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It transfers the tokens that the user is selling to the solver - * @param solverOp The SolverOperation that is about to execute - * @return true if the transfer was successful, false otherwise - */ - function _preSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override { - SwapData memory swapData = abi.decode(returnData, (SwapData)); - - // Optimistically transfer to the solver contract the tokens that the user is selling - _transferUserERC20(swapData.tokenUserSells, solverOp.solver, swapData.amountUserSells); - - return; // success - } - - /* - * @notice This function is called after a solver operation executed - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It transfers to the user the tokens they are buying - * @param _ - * @param returnData The return data from the user operation (swap data) - * @return true if the transfer was successful, false otherwise - */ - function _postSolverCall(SolverOperation calldata, bytes calldata returnData) internal override { - SwapData memory swapData = abi.decode(returnData, (SwapData)); - uint256 buyTokenBalance = IERC20(swapData.tokenUserBuys).balanceOf(address(this)); - - if (buyTokenBalance < swapData.amountUserBuys) { - revert(); - } - - // Transfer exactly the amount the user is buying, the bid amount will be transferred - // in _allocateValueCall, even if those are the same tokens - if (swapData.tokenUserBuys != swapData.auctionBaseCurrency) { - SafeTransferLib.safeTransfer(swapData.tokenUserBuys, _user(), buyTokenBalance); - } else { - SafeTransferLib.safeTransfer(swapData.tokenUserBuys, _user(), swapData.amountUserBuys); - } - - return; // success - } - - /* - * @notice This function is called after a solver has successfully paid their bid - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It transfers all the available bid tokens on the contract (instead of only the bid amount, - * to avoid leaving any dust on the contract) - * @param bidToken The address of the token used for the winning solver operation's bid - * @param _ - * @param _ - */ - function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata) internal override { - if (bidToken == address(0)) { - SafeTransferLib.safeTransferETH(_user(), bidAmount); - } else { - SafeTransferLib.safeTransfer(bidToken, _user(), bidAmount); - } - } - - // ---------------------------------------------------- // - // Getters and helpers // - // ---------------------------------------------------- // - - function getBidFormat(UserOperation calldata userOp) public pure override returns (address bidToken) { - (SwapIntent memory swapIntent) = abi.decode(userOp.data[4:], (SwapIntent)); - bidToken = swapIntent.auctionBaseCurrency; - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } -} diff --git a/src/contracts/examples/intents-example/SwapIntentInvertBidDAppControl.sol b/src/contracts/examples/intents-example/SwapIntentInvertBidDAppControl.sol deleted file mode 100644 index 48b72c81d..000000000 --- a/src/contracts/examples/intents-example/SwapIntentInvertBidDAppControl.sol +++ /dev/null @@ -1,180 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.16; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Base Imports -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; - -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Atlas DApp-Control Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; - -import "forge-std/Test.sol"; - -/** - * @notice SwapIntent where user wants exact amount of `tokenUserBuys` and is willing to sell up to `maxAmountUserSells` - * of - * `tokenUserSells` for it - */ -struct SwapIntent { - address tokenUserBuys; - address tokenUserSells; - uint256 amountUserBuys; - uint256 maxAmountUserSells; -} - -/** - * @title SwapIntentInvertBidDAppControl - * @notice A DAppControl contract that allows a user to swap tokens with a solver using a `SwapIntent` - * @dev The invertBidValue flag is set to true - */ -contract SwapIntentInvertBidDAppControl is DAppControl { - bool public immutable _solverBidRetrievalRequired; - - constructor( - address _atlas, - bool solverBidRetrievalRequired - ) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: false, - trackPreOpsReturnData: false, - trackUserReturnData: true, - delegateUser: true, - requirePreSolver: true, - requirePostSolver: true, - requirePostOps: false, - zeroSolvers: false, - reuseUserOp: true, - userAuctioneer: false, - solverAuctioneer: false, - unknownAuctioneer: false, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: true, - trustedOpHash: true, - invertBidValue: true, - exPostBids: false, - allowAllocateValueFailure: true - }) - ) - { - _solverBidRetrievalRequired = solverBidRetrievalRequired; - } - - // ---------------------------------------------------- // - // Custom // - // ---------------------------------------------------- // - - /* - * @notice This is the user operation target function - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It checks that the user has approved Atlas to spend the tokens they are selling and the conditions are met - * @param swapIntent The SwapIntent struct - */ - function swap(SwapIntent calldata swapIntent) external payable returns (SwapIntent memory) { - require(msg.sender == ATLAS, "SwapIntentDAppControl: InvalidSender"); - require(address(this) != CONTROL, "SwapIntentDAppControl: MustBeDelegated"); - - // Transfer to the Execution Environment the amount that the solver is invert bidding - _transferUserERC20(swapIntent.tokenUserSells, address(this), swapIntent.maxAmountUserSells); - - return SwapIntent({ - tokenUserBuys: swapIntent.tokenUserBuys, - tokenUserSells: swapIntent.tokenUserSells, - amountUserBuys: swapIntent.amountUserBuys, - maxAmountUserSells: swapIntent.maxAmountUserSells - }); - } - - // ---------------------------------------------------- // - // Atlas hooks // - // ---------------------------------------------------- // - - /* - * @notice This function is called before a solver operation executes - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It transfers the tokens that the user is selling to the solver - * @param solverOp The SolverOperation that is about to execute - * @return true if the transfer was successful, false otherwise - */ - function _preSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override { - address solverTo = solverOp.solver; - SwapIntent memory swapData = abi.decode(returnData, (SwapIntent)); - - // The solver must be bidding less than the intent's maxAmountUserSells - require(solverOp.bidAmount <= swapData.maxAmountUserSells, "SwapIntentInvertBid: BidTooHigh"); - - if (_solverBidRetrievalRequired) { - // Approve solver to take their bidAmount of the token the user is selling - // _getAndApproveUserERC20(swapData.tokenUserSells, solverOp.bidAmount, solverTo); - SafeTransferLib.safeApprove(swapData.tokenUserSells, solverTo, solverOp.bidAmount); - } else { - // Optimistically transfer to the solver contract the amount that the solver is invert bidding - // _transferUserERC20(swapData.tokenUserSells, solverTo, solverOp.bidAmount); - SafeTransferLib.safeTransfer(swapData.tokenUserSells, solverTo, solverOp.bidAmount); - } - } - - /* - * @notice This function is called after a solver operation executed - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It transfers to the user the tokens they are buying - * @param _ - * @param returnData The return data from the user operation (swap data) - * @return true if the transfer was successful, false otherwise - */ - function _postSolverCall(SolverOperation calldata, bytes calldata returnData) internal override { - SwapIntent memory swapIntent = abi.decode(returnData, (SwapIntent)); - uint256 buyTokenBalance = IERC20(swapIntent.tokenUserBuys).balanceOf(address(this)); - - if (buyTokenBalance < swapIntent.amountUserBuys) { - revert("SwapIntentInvertBid: Intent Unfulfilled - buyTokenBalance < amountUserBuys"); - } - - // Transfer the tokens the user is buying to the user - SafeTransferLib.safeTransfer(swapIntent.tokenUserBuys, _user(), swapIntent.amountUserBuys); - } - - /* - * @notice This function is called after a solver has successfully paid their bid - * @dev This function transfers any excess `tokenUserSells` tokens back to the user - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It transfers all the available bid tokens on the contract (instead of only the bid amount, - * to avoid leaving any dust on the contract) - * @param bidToken The address of the token used for the winning solver operation's bid - * @param bidAmount The winning bid amount - * @param _ - */ - function _allocateValueCall(address bidToken, uint256, bytes calldata) internal override { - if (bidToken == address(0)) { - SafeTransferLib.safeTransferETH(_user(), address(this).balance); - } else { - SafeTransferLib.safeTransfer(bidToken, _user(), IERC20(bidToken).balanceOf(address(this))); - } - } - - // ---------------------------------------------------- // - // Getters and helpers // - // ---------------------------------------------------- // - - function getBidFormat(UserOperation calldata userOp) public pure override returns (address bidToken) { - (SwapIntent memory swapIntent) = abi.decode(userOp.data[4:], (SwapIntent)); - bidToken = swapIntent.tokenUserSells; - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } -} diff --git a/src/contracts/examples/intents-example/V4SwapIntent.sol b/src/contracts/examples/intents-example/V4SwapIntent.sol deleted file mode 100644 index 46c6f2775..000000000 --- a/src/contracts/examples/intents-example/V4SwapIntent.sol +++ /dev/null @@ -1,272 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.16; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Base Imports -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; - -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Atlas DApp-Control Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; - -import "forge-std/Test.sol"; - -// This struct is for passing around data internally -struct SwapData { - address tokenIn; - address tokenOut; - int256 requestedAmount; // positive for exact in, negative for exact out - uint256 limitAmount; // if exact in, min amount out. if exact out, max amount in - address recipient; -} - -contract V4SwapIntentControl is DAppControl { - address immutable V4_POOL; // TODO: set for test v4 pool - - uint256 startingBalance; // Balance tracked for the v4 pool - - constructor( - address _atlas, - address poolManager - ) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: false, - trackPreOpsReturnData: false, - trackUserReturnData: true, - delegateUser: true, - requirePreSolver: true, - requirePostSolver: true, - requirePostOps: false, - zeroSolvers: false, - reuseUserOp: true, - userAuctioneer: true, - solverAuctioneer: true, - verifyCallChainHash: true, - unknownAuctioneer: true, - forwardReturnData: false, - requireFulfillment: true, - trustedOpHash: false, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { - V4_POOL = poolManager; - } - - ////////////////////////////////// - // CONTRACT-SPECIFIC FUNCTIONS // - ////////////////////////////////// - - modifier verifyCall(address tokenIn, address tokenOut, uint256 amount) { - require(msg.sender == ATLAS, "ERR-PI002 InvalidSender"); - require(address(this) != CONTROL, "ERR-PI004 MustBeDelegated"); - - address user = _user(); - - // TODO: Could maintain a balance of "1" of each token to allow the user to save gas over multiple uses - uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); - if (tokenInBalance > 0) { - SafeTransferLib.safeTransfer(tokenIn, user, tokenInBalance); - } - - uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); - if (tokenOutBalance > 0) { - SafeTransferLib.safeTransfer(tokenOut, user, tokenOutBalance); - } - - require(_availableFundsERC20(tokenIn, user, amount, ExecutionPhase.PreSolver), "ERR-PI059 SellFundsUnavailable"); - _; - } - - struct ExactInputSingleParams { - address tokenIn; - address tokenOut; - uint256 maxFee; - address recipient; - uint256 amountIn; - uint256 amountOutMinimum; - uint256 sqrtPriceLimitX96; - } - - // selector 0x04e45aaf - function exactInputSingle(ExactInputSingleParams calldata params) - external - payable - verifyCall(params.tokenIn, params.tokenOut, params.amountIn) - returns (SwapData memory) - { - SwapData memory swapData = SwapData({ - tokenIn: params.tokenIn, - tokenOut: params.tokenOut, - requestedAmount: int256(params.amountIn), - limitAmount: params.amountOutMinimum, - recipient: params.recipient - }); - - return swapData; - } - - struct ExactOutputSingleParams { - address tokenIn; - address tokenOut; - uint256 maxFee; - address recipient; - uint256 amountInMaximum; - uint256 amountOut; - uint256 sqrtPriceLimitX96; - } - - // selector 0x5023b4df - function exactOutputSingle(ExactOutputSingleParams calldata params) - external - payable - verifyCall(params.tokenIn, params.tokenOut, params.amountInMaximum) - returns (SwapData memory) - { - SwapData memory swapData = SwapData({ - tokenIn: params.tokenIn, - tokenOut: params.tokenOut, - requestedAmount: -int256(params.amountOut), - limitAmount: params.amountInMaximum, - recipient: params.recipient - }); - - return swapData; - } - - ////////////////////////////////// - // ATLAS OVERRIDE FUNCTIONS // - ////////////////////////////////// - - function _preSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override { - address solverTo = solverOp.solver; - if (solverTo == address(this) || solverTo == _control() || solverTo == ATLAS) { - revert(); - } - - SwapData memory swapData = abi.decode(returnData, (SwapData)); - - // Record balance and transfer to the solver - if (swapData.requestedAmount > 0) { - // exact input - startingBalance = IERC20(swapData.tokenIn).balanceOf(V4_POOL); - _transferUserERC20(swapData.tokenIn, solverTo, uint256(swapData.requestedAmount)); - } else { - // exact output - startingBalance = IERC20(swapData.tokenOut).balanceOf(V4_POOL); - _transferUserERC20(swapData.tokenIn, solverTo, swapData.limitAmount - solverOp.bidAmount); - // For exact output swaps, the solver solvers compete and bid on how much tokens they can - // return to the user in excess of their specified limit input. We only transfer what they - // require to make the swap in this step. - } - - // TODO: Permit69 is currently enabled during solver phase, but there is low conviction that this - // does not enable an attack vector. Consider enabling to save gas on a transfer? - } - - // Checking intent was fulfilled, and user has received their tokens, happens here - function _postSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override { - SwapData memory swapData = abi.decode(returnData, (SwapData)); - - uint256 buyTokenBalance = IERC20(swapData.tokenOut).balanceOf(address(this)); - uint256 amountUserBuys = - swapData.requestedAmount > 0 ? swapData.limitAmount : uint256(-swapData.requestedAmount); - - // If it was an exact input swap, we need to verify that - // a) We have enough tokens to meet the user's minimum amount out - // b) The output amount matches (or is greater than) the solver's bid - // c) PoolManager's balances increased by the provided input amount - if (swapData.requestedAmount > 0) { - if (buyTokenBalance < swapData.limitAmount) { - revert(); // insufficient amount out - } - if (buyTokenBalance < solverOp.bidAmount) { - revert(); // does not meet solver bid - } - uint256 endingBalance = IERC20(swapData.tokenIn).balanceOf(V4_POOL); - if ((endingBalance - startingBalance) < uint256(swapData.requestedAmount)) { - revert(); // pool manager balances did not increase by the provided input amount - } - } else { - // Exact output swap - check the output amount was transferred out by pool - uint256 endingBalance = IERC20(swapData.tokenOut).balanceOf(V4_POOL); - if ((startingBalance - endingBalance) < amountUserBuys) { - revert(); // pool manager balances did not decrease by the provided output amount - } - } - // no need to check for exact output, since the max is whatever the user transferred - - if (buyTokenBalance >= amountUserBuys) { - // Make sure not to transfer any extra 'auctionBaseCurrency' token, since that will be used - // for the auction measurements - address auctionBaseCurrency = swapData.requestedAmount > 0 ? swapData.tokenOut : swapData.tokenIn; - - if (swapData.tokenOut != auctionBaseCurrency) { - SafeTransferLib.safeTransfer(swapData.tokenOut, swapData.recipient, buyTokenBalance); - } else { - SafeTransferLib.safeTransfer(swapData.tokenOut, swapData.recipient, amountUserBuys); - } - return; // only success case of this hook - } - - revert(); - } - - /* - * @notice This function is called after a solver has successfully paid their bid - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev transfers the bid amount to the user supports native ETH and ERC20 tokens - * @param bidToken The address of the token used for the winning SolverOperation's bid - * @param bidAmount The winning bid amount - * @param _ - */ - function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata) internal override { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Atlas - if (bidToken == address(0)) { - SafeTransferLib.safeTransferETH(_user(), bidAmount); - } else { - SafeTransferLib.safeTransfer(bidToken, _user(), bidAmount); - } - } - - ///////////////////////////////////////////////////////// - ///////////////// GETTERS & HELPERS // ////////////////// - ///////////////////////////////////////////////////////// - // NOTE: These are not delegatecalled - - function getBidFormat(UserOperation calldata userOp) public pure override returns (address bidToken) { - // This is a helper function called by solvers - // so that they can get the proper format for - // submitting their bids to the hook. - - if (bytes4(userOp.data[:4]) == this.exactInputSingle.selector) { - // exact input swap, the bidding is done in output token - (, bidToken) = abi.decode(userOp.data[4:], (address, address)); - } else if (bytes4(userOp.data[:4]) == this.exactOutputSingle.selector) { - // exact output, bidding done in input token - bidToken = abi.decode(userOp.data[4:], (address)); - } - - // should we return an error here if the function is wrong? - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } -} diff --git a/src/contracts/examples/oev-example/ChainlinkAtlasWrapper.sol b/src/contracts/examples/oev-example/ChainlinkAtlasWrapper.sol deleted file mode 100644 index 5fb5df22c..000000000 --- a/src/contracts/examples/oev-example/ChainlinkAtlasWrapper.sol +++ /dev/null @@ -1,249 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import { SafeERC20, IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import { - IChainlinkAtlasWrapper, - AggregatorV2V3Interface -} from "src/contracts/examples/oev-example/IChainlinkAtlasWrapper.sol"; -import { IChainlinkDAppControl } from "src/contracts/examples/oev-example/IChainlinkDAppControl.sol"; - -// A wrapper contract for a specific Chainlink price feed, used by Atlas to capture Oracle Extractable Value (OEV). -// Each MEV-generating protocol needs their own wrapper for each Chainlink price feed they use. -contract ChainlinkAtlasWrapper is Ownable, IChainlinkAtlasWrapper { - address public immutable ATLAS; - AggregatorV2V3Interface public immutable BASE_FEED; // Base Chainlink Feed - IChainlinkDAppControl public immutable DAPP_CONTROL; // Chainlink Atlas DAppControl - - // Vars below hold values from the most recent successful `transmit()` call to this wrapper. - int256 public atlasLatestAnswer; - uint256 public atlasLatestTimestamp; - uint40 public atlasLatestEpochAndRound; - - // Trusted ExecutionEnvironments - mapping(address transmitter => bool trusted) public transmitters; - - error TransmitterNotTrusted(address transmitter); - error ArrayLengthMismatch(); - error CannotReuseReport(); - error ZeroObservations(); - error ObservationsNotOrdered(); - error ObservationsMustBePositive(); - error SignerVerificationFailed(); - error WithdrawETHFailed(); - - event TransmitterStatusChanged(address indexed transmitter, bool trusted); - - constructor(address atlas, address baseChainlinkFeed, address _owner) Ownable(_owner) { - ATLAS = atlas; - BASE_FEED = AggregatorV2V3Interface(baseChainlinkFeed); - DAPP_CONTROL = IChainlinkDAppControl(msg.sender); // Chainlink DAppControl is also wrapper factory - } - - // ---------------------------------------------------- // - // Atlas Impl Functions // - // ---------------------------------------------------- // - - // Called by a trusted ExecutionEnvironment during an Atlas metacall - // Returns address of this contract - used in allocateValueCall for OEV allocation - function transmit( - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - external - returns (address) - { - if (!transmitters[msg.sender]) revert TransmitterNotTrusted(msg.sender); - if (rs.length != ss.length) revert ArrayLengthMismatch(); - - (int256 answer, uint40 epochAndRound) = _verifyTransmitData(report, rs, ss, rawVs); - - atlasLatestAnswer = answer; - atlasLatestTimestamp = block.timestamp; - atlasLatestEpochAndRound = epochAndRound; - - return address(this); - } - - // Verification checks for new transmit data - function _verifyTransmitData( - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - internal - view - returns (int256, uint40) - { - int192 median; - uint40 epochAndRound; - ReportData memory r; - (r.rawReportContext,, r.observations) = abi.decode(report, (bytes32, bytes32, int192[])); - - // New stack frame required here to avoid Stack Too Deep error - { - uint256 observationCount = r.observations.length; - if (observationCount == 0) revert ZeroObservations(); - - // Check report data has not already been used in this wrapper - epochAndRound = uint40(uint256(r.rawReportContext)); - if (epochAndRound <= atlasLatestEpochAndRound) revert CannotReuseReport(); - - // Check observations are ordered - for (uint256 i; i < observationCount - 1; ++i) { - bool inOrder = r.observations[i] <= r.observations[i + 1]; - if (!inOrder) revert ObservationsNotOrdered(); - } - if (r.observations[0] < 0) revert ObservationsMustBePositive(); - - // Calculate median from observations, cannot be 0 - median = r.observations[observationCount / 2]; - } - - bool signersVerified = - IChainlinkDAppControl(DAPP_CONTROL).verifyTransmitSigners(address(BASE_FEED), report, rs, ss, rawVs); - if (!signersVerified) revert SignerVerificationFailed(); - - return (int256(median), epochAndRound); - } - - // ---------------------------------------------------- // - // Chainlink Pass-through Functions // - // ---------------------------------------------------- // - - // Called by the contract which creates OEV when reading a price feed update. - // If Atlas solvers have submitted a more recent answer than the base oracle's most recent answer, - // the `atlasLatestAnswer` will be returned. Otherwise fallback to the base oracle's answer. - function latestAnswer() public view returns (int256) { - if (BASE_FEED.latestTimestamp() >= atlasLatestTimestamp) { - return BASE_FEED.latestAnswer(); - } - - return atlasLatestAnswer; - } - - // Use this contract's latestTimestamp if more recent than base oracle's. - // Otherwise fallback to base oracle's latestTimestamp - function latestTimestamp() public view returns (uint256) { - uint256 baseFeedLatestTimestamp = BASE_FEED.latestTimestamp(); - if (baseFeedLatestTimestamp >= atlasLatestTimestamp) { - return baseFeedLatestTimestamp; - } - - return atlasLatestTimestamp; - } - - // Pass-through call to base oracle's `latestRound()` function - function latestRound() external view override returns (uint256) { - return BASE_FEED.latestRound(); - } - - // Fallback to base oracle's latestRoundData, unless this contract's `latestTimestamp` and `latestAnswer` are more - // recent, in which case return those values as well as the other round data from the base oracle. - // NOTE: This may break some integrations as it implies a `roundId` has multiple answers (the canonical answer from - // the base feed, and the `atlasLatestAnswer` if more recent), which deviates from the expected behaviour of the - // base Chainlink feeds. Be aware of this tradeoff when integrating ChainlinkAtlasWrappers as your price feed. - function latestRoundData() - public - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - (roundId, answer, startedAt, updatedAt, answeredInRound) = BASE_FEED.latestRoundData(); - if (updatedAt < atlasLatestTimestamp) { - answer = atlasLatestAnswer; - updatedAt = atlasLatestTimestamp; - } - } - - // Pass-through call to base oracle's `getAnswer()` function - function getAnswer(uint256 roundId) external view override returns (int256) { - return BASE_FEED.getAnswer(roundId); - } - - // Pass-through call to base oracle's `getTimestamp()` function - function getTimestamp(uint256 roundId) external view override returns (uint256) { - return BASE_FEED.getTimestamp(roundId); - } - - // Pass-through call to base oracle's `getRoundData()` function - function getRoundData(uint80 _roundId) - external - view - override - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - return BASE_FEED.getRoundData(_roundId); - } - - // Pass-through calls to base oracle's `decimals()` functions - function decimals() external view override returns (uint8) { - return BASE_FEED.decimals(); - } - - // Pass-through calls to base oracle's `description()` functions - function description() external view override returns (string memory) { - return BASE_FEED.description(); - } - - // Pass-through calls to base oracle's `version()` functions - function version() external view override returns (uint256) { - return BASE_FEED.version(); - } - - // ---------------------------------------------------- // - // Owner Functions // - // ---------------------------------------------------- // - - // Owner can add/remove trusted transmitters (ExecutionEnvironments) - function setTransmitterStatus(address transmitter, bool trusted) external onlyOwner { - transmitters[transmitter] = trusted; - emit TransmitterStatusChanged(transmitter, trusted); - } - - // Withdraw ETH OEV captured via Atlas solver bids - function withdrawETH(address recipient) external onlyOwner { - (bool success,) = recipient.call{ value: address(this).balance }(""); - if (!success) revert WithdrawETHFailed(); - } - - // Withdraw ERC20 OEV captured via Atlas solver bids - function withdrawERC20(address token, address recipient) external onlyOwner { - SafeERC20.safeTransfer(IERC20(token), recipient, IERC20(token).balanceOf(address(this))); - } - - fallback() external payable { } - - receive() external payable { } -} - -// ---------------------------------------------------- // -// Chainlink Aggregator Structs // -// ---------------------------------------------------- // - -struct ReportData { - HotVars hotVars; // Only read from storage once - bytes observers; // ith element is the index of the ith observer - int192[] observations; // ith element is the ith observation - bytes vs; // jth element is the v component of the jth signature - bytes32 rawReportContext; -} - -struct HotVars { - // Provides 128 bits of security against 2nd pre-image attacks, but only - // 64 bits against collisions. This is acceptable, since a malicious owner has - // easier way of messing up the protocol than to find hash collisions. - bytes16 latestConfigDigest; - uint40 latestEpochAndRound; // 32 most sig bits for epoch, 8 least sig bits for round - // Current bound assumed on number of faulty/dishonest oracles participating - // in the protocol, this value is referred to as f in the design - uint8 threshold; - // Chainlink Aggregators expose a roundId to consumers. The offchain reporting - // protocol does not use this id anywhere. We increment it whenever a new - // transmission is made to provide callers with contiguous ids for successive - // reports. - uint32 latestAggregatorRoundId; -} diff --git a/src/contracts/examples/oev-example/ChainlinkAtlasWrapperAlt.sol b/src/contracts/examples/oev-example/ChainlinkAtlasWrapperAlt.sol deleted file mode 100644 index 47cefee0d..000000000 --- a/src/contracts/examples/oev-example/ChainlinkAtlasWrapperAlt.sol +++ /dev/null @@ -1,355 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import { SafeERC20, IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -// A wrapper contract for a specific Chainlink price feed, used by Atlas to capture Oracle Extractable Value (OEV). -// Each MEV-generating protocol needs their own wrapper for each Chainlink price feed they use. -contract ChainlinkAtlasWrapper is Ownable { - address public immutable ATLAS; - IChainlinkFeed public immutable BASE_FEED; // Base Chainlink Feed - IChainlinkDAppControl public immutable DAPP_CONTROL; // Chainlink Atlas DAppControl - - int256 public atlasLatestAnswer; - uint256 public atlasLatestTimestamp; - - uint256 private immutable _GAS_THRESHOLD; - - error TransmitterInvalid(address transmitter); - error InvalidTransmitMsgDataLength(); - error ObservationsNotOrdered(); - error AnswerMustBeAboveZero(); - error SignerVerificationFailed(); - error WithdrawETHFailed(); - - event SignerStatusChanged(address indexed account, bool isSigner); - - constructor(address atlas, address baseChainlinkFeed, address _owner) Ownable(_owner) { - ATLAS = atlas; - BASE_FEED = IChainlinkFeed(baseChainlinkFeed); - DAPP_CONTROL = IChainlinkDAppControl(msg.sender); // Chainlink DAppControl is also wrapper factory - - // do a gas usage check on an invalid trasmitting address - address _aggregator = BASE_FEED.aggregator(); - - // heat up the address - IOffchainAggregator(_aggregator).oracleObservationCount{ gas: 10_000 }(_owner); - - // get the gas usage of an Unset address - uint256 gasUsed = gasleft(); - IOffchainAggregator(_aggregator).oracleObservationCount{ gas: 10_000 }(atlas); - gasUsed -= gasleft(); - - _GAS_THRESHOLD = gasUsed + 199; // 199 = warm SLOADx2 - 1 - - address transmitter = IOffchainAggregator(_aggregator).transmitters()[2]; - // heat up the second storage slot - IOffchainAggregator(_aggregator).oracleObservationCount{ gas: 10_000 }(transmitter); - // change to next transmitter (packed w/ prev one in second storage slot) - transmitter = IOffchainAggregator(_aggregator).transmitters()[3]; - - // check gas used - gasUsed = gasleft(); - IOffchainAggregator(_aggregator).oracleObservationCount{ gas: 10_000 }(transmitter); - gasUsed -= gasleft(); - - require(gasUsed > _GAS_THRESHOLD, "invalid gas threshold"); - } - - // Called by the contract which creates OEV when reading a price feed update. - // If Atlas solvers have submitted a more recent answer than the base oracle's most recent answer, - // the `atlasLatestAnswer` will be returned. Otherwise fallback to the base oracle's answer. - function latestAnswer() public view returns (int256) { - if (BASE_FEED.latestTimestamp() >= atlasLatestTimestamp) { - return BASE_FEED.latestAnswer(); - } - - return atlasLatestAnswer; - } - - // Use this contract's latestTimestamp if more recent than base oracle's. - // Otherwise fallback to base oracle's latestTimestamp - function latestTimestamp() public view returns (uint256) { - if (BASE_FEED.latestTimestamp() >= atlasLatestTimestamp) { - return BASE_FEED.latestTimestamp(); - } - - return atlasLatestTimestamp; - } - - // Fallback to base oracle's latestRoundData, unless this contract's latestTimestamp and latestAnswer are more - // recent, in which case return those values as well as the other round data from the base oracle. - function latestRoundData() - public - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - (roundId, answer, startedAt, updatedAt, answeredInRound) = BASE_FEED.latestRoundData(); - if (updatedAt < atlasLatestTimestamp) { - answer = atlasLatestAnswer; - updatedAt = atlasLatestTimestamp; - } - } - - // Called by a trusted ExecutionEnvironment during an Atlas metacall - // Returns address of this contract - used in allocateValueCall for OEV allocation - function transmit( - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - external - returns (address) - { - int256 answer = _verifyTransmitData(report, rs, ss, rawVs); - - atlasLatestAnswer = answer; - atlasLatestTimestamp = block.timestamp; - - return address(this); - } - - // Verification checks for new transmit data - function _verifyTransmitData( - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - internal - view - returns (int256) - { - ReportData memory r; - (,, r.observations) = abi.decode(report, (bytes32, bytes32, int192[])); - - // Check observations are ordered, then take median observation - for (uint256 i; i < r.observations.length - 1; ++i) { - bool inOrder = r.observations[i] <= r.observations[i + 1]; - if (!inOrder) revert ObservationsNotOrdered(); - } - - (address[] memory validTransmitters, address _aggregator) = _validateTransmitter(); - - bytes32 reportHash = keccak256(report); - - for (uint256 i; i < rs.length; ++i) { - address signer = ecrecover(reportHash, uint8(rawVs[i]) + 27, rs[i], ss[i]); - if (!_isSigner(validTransmitters, _aggregator, signer)) { - // console.log("invalid signer:", signer); - revert(); - } - // console.log("__valid signer:", signer); - } - - int192 median = r.observations[r.observations.length / 2]; - - if (median <= 0) revert AnswerMustBeAboveZero(); - - return int256(median); - } - - function _validateTransmitter() internal view returns (address[] memory validTransmitters, address _aggregator) { - // Get the user from the EE - // NOTE: technically we can pull this from calldata, including full function here for readability - address transmitter = IExecutionEnvironment(msg.sender).getUser(); - - // Verify that the execution environment (msg.sender) is genuine - // NOTE: Technically we can skip this too since the activeEnvironment check below also validates this - (address _executionEnvironment,,) = - IAtlasFactory(ATLAS).getExecutionEnvironment(transmitter, address(DAPP_CONTROL)); - - if (msg.sender != _executionEnvironment) { - revert TransmitterInvalid(transmitter); - } - - if (IExecutionEnvironment(msg.sender).getControl() != address(DAPP_CONTROL)) { - revert TransmitterInvalid(transmitter); - } - - // Verify that this environment is the currently active one according to Atlas - // require(msg.sender == _activeEnvironment(), "inactive EE"); - - // Get the valid transmitters - // NOTE: Be careful if considering storing these in a map - that would make it tricky to deauthorize for this - // contract - // when they're deauthorized on the parent aggregator. imo it's better to skip the map altogether since we'd - // want a - //fully updated list each time to make sure no transmitter has been removed. - _aggregator = BASE_FEED.aggregator(); - validTransmitters = IOffchainAggregator(_aggregator).transmitters(); - - // Make sure this transmitter is valid - if (!_isTransmitter(validTransmitters, transmitter)) { - revert TransmitterInvalid(transmitter); - } - - // Heat up the storage access on the s_oracle array loc/length so that _isSigner()'s gasleft() checks are even - IOffchainAggregator(_aggregator).oracleObservationCount(transmitter); - } - - function _isTransmitter(address[] memory validTransmitters, address transmitter) internal pure returns (bool) { - uint256 len = validTransmitters.length; - // Loop through them and see if there's a match - for (uint256 i; i < len; i++) { - if (transmitter == validTransmitters[i]) return true; - } - return false; - } - - function _isSigner( - address[] memory validTransmitters, - address _aggregator, - address signer - ) - internal - view - returns (bool) - { - /* - Super hacky approach... but if an address isn't "Role.Unset" and it also isn't - Role.Transmitter then by the process of elimination that means it's a valid signer. - - We can determine if it's Unset or not by the gas used for the view call: - Unset = 1 storage read, Transmitter or Signer = 2 storage reads: - - function oracleObservationCount(address _signerOrTransmitter) { - Oracle memory oracle = s_oracles[_signerOrTransmitter]; - if (oracle.role == Role.Unset) { return 0; } - return s_oracleObservationsCounts[oracle.index] - 1; - } - - s_oracles[i] should be a cold storage load - if not, it's invalid. - s_oracleObservationsCounts[i] may be cold or hot - it's a packed struct. - _aggregator is a hot address. - */ - - uint256 gasUsed = gasleft(); - IOffchainAggregator(_aggregator).oracleObservationCount{ gas: 10_000 }(signer); - gasUsed -= gasleft(); - - /* - console.log("---"); - console.log("signer:", signer); - console.log("gas used:", gasUsed); - */ - - // NOTE: The gas usage check also will fail if a signer doublesigns - if (gasUsed > _GAS_THRESHOLD) { - // TODO: pinpoint the actual threshold, add in index + packing. - return !_isTransmitter(validTransmitters, signer); - } - return false; - } - - // ---------------------------------------------------- // - // Owner Functions // - // ---------------------------------------------------- // - - // Withdraw ETH OEV captured via Atlas solver bids - function withdrawETH(address recipient) external onlyOwner { - (bool success,) = recipient.call{ value: address(this).balance }(""); - if (!success) revert WithdrawETHFailed(); - } - - // Withdraw ERC20 OEV captured via Atlas solver bids - function withdrawERC20(address token, address recipient) external onlyOwner { - SafeERC20.safeTransfer(IERC20(token), recipient, IERC20(token).balanceOf(address(this))); - } - - fallback() external payable { } - receive() external payable { } - - // ---------------------------------------------------- // - // View Functions // - // ---------------------------------------------------- // - function aggregator() external view returns (address) { - return BASE_FEED.aggregator(); - } - - function transmitters() external view returns (address[] memory) { - return IOffchainAggregator(BASE_FEED.aggregator()).transmitters(); - } - - function executionEnvironment(address transmitter) external view returns (address environment) { - (environment,,) = IAtlasFactory(ATLAS).getExecutionEnvironment(transmitter, address(DAPP_CONTROL)); - } -} - -// ----------------------------------------------- -// Structs and interface for Chainlink Aggregator -// ----------------------------------------------- - -struct ReportData { - HotVars hotVars; // Only read from storage once - bytes observers; // ith element is the index of the ith observer - int192[] observations; // ith element is the ith observation - bytes vs; // jth element is the v component of the jth signature - bytes32 rawReportContext; -} - -struct HotVars { - // Provides 128 bits of security against 2nd pre-image attacks, but only - // 64 bits against collisions. This is acceptable, since a malicious owner has - // easier way of messing up the protocol than to find hash collisions. - bytes16 latestConfigDigest; - uint40 latestEpochAndRound; // 32 most sig bits for epoch, 8 least sig bits for round - // Current bound assumed on number of faulty/dishonest oracles participating - // in the protocol, this value is referred to as f in the design - uint8 threshold; - // Chainlink Aggregators expose a roundId to consumers. The offchain reporting - // protocol does not use this id anywhere. We increment it whenever a new - // transmission is made to provide callers with contiguous ids for successive - // reports. - uint32 latestAggregatorRoundId; -} - -interface IChainlinkFeed { - function latestAnswer() external view returns (int256); - function latestTimestamp() external view returns (uint256); - function latestRoundData() - external - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); - function owner() external view returns (address); - function aggregator() external view returns (address); - function phaseId() external view returns (uint16); - function phaseAggregators(uint16 phaseId) external view returns (address); - function proposedAggregator() external view returns (address); -} - -interface IOffchainAggregator { - function transmitters() external view returns (address[] memory); - function oracleObservationCount(address _signerOrTransmitter) external view returns (uint16); -} - -interface IExecutionEnvironment { - function getUser() external pure returns (address user); - function getControl() external pure returns (address control); -} - -interface IAtlasFactory { - function activeEnvironment() external view returns (address); - function getExecutionEnvironment( - address user, - address dAppControl - ) - external - view - returns (address executionEnvironment, uint32 callConfig, bool exists); -} - -interface IChainlinkDAppControl { - function verifyTransmitSigners( - address baseChainlinkFeed, - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - external - view - returns (bool verified); -} diff --git a/src/contracts/examples/oev-example/ChainlinkDAppControl.sol b/src/contracts/examples/oev-example/ChainlinkDAppControl.sol deleted file mode 100644 index ab190d88a..000000000 --- a/src/contracts/examples/oev-example/ChainlinkDAppControl.sol +++ /dev/null @@ -1,263 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -import { ECDSA } from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { ChainlinkAtlasWrapper } from "src/contracts/examples/oev-example/ChainlinkAtlasWrapper.sol"; - -// Role enum as per Chainlink's OffchainAggregatorBilling.sol contract -enum Role { - // No oracle role has been set for address a - Unset, - // Signing address a of an oracle. I.e. report signatures from this oracle should ecrecover back to address a. - Signer, - // Transmitter role is not used - Transmitter -} - -struct Oracle { - uint8 index; // Index of oracle in signers array - Role role; // Role of the address which mapped to this struct -} - -struct VerificationVars { - mapping(address signer => Oracle oracle) oracles; - address[] signers; -} - -// NOTE: This contract acts as the Chainlink DAppControl for Atlas, -// and as a factory for ChainlinkAtlasWrapper contracts -contract ChainlinkDAppControl is DAppControl { - uint256 public constant MAX_NUM_ORACLES = 31; - - uint256 public observationsQuorum = 1; - - mapping(address baseChainlinkFeed => VerificationVars) internal verificationVars; - mapping(address chainlinkWrapper => bool isWrapper) public isChainlinkWrapper; - - error InvalidBaseFeed(); - error FailedToAllocateOEV(); - error OnlyGovernance(); - error SignerNotFound(); - error TooManySigners(); - error DuplicateSigner(address signer); - error InvalidChainlinkAtlasWrapper(); - - event NewChainlinkWrapperCreated(address indexed wrapper, address indexed baseFeed, address indexed owner); - event SignersSetForBaseFeed(address indexed baseFeed, address[] signers); - event SignerAddedForBaseFeed(address indexed baseFeed, address indexed signer); - event SignerRemovedForBaseFeed(address indexed baseFeed, address indexed signer); - event ObservationsQuorumSet(uint256 oldQuorum, uint256 newQuorum); - - constructor(address _atlas) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: false, - trackPreOpsReturnData: false, - trackUserReturnData: true, - delegateUser: false, - requirePreSolver: false, - requirePostSolver: false, - requirePostOps: false, - zeroSolvers: false, - reuseUserOp: false, - userAuctioneer: true, - solverAuctioneer: false, - unknownAuctioneer: true, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: false, // Update oracle even if all solvers fail - trustedOpHash: true, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { } - - // ---------------------------------------------------- // - // Atlas Hook Overrides // - // ---------------------------------------------------- // - - function _allocateValueCall(address, uint256 bidAmount, bytes calldata data) internal virtual override { - address chainlinkWrapper = abi.decode(data, (address)); - if (!ChainlinkDAppControl(_control()).isChainlinkWrapper(chainlinkWrapper)) { - revert InvalidChainlinkAtlasWrapper(); - } - (bool success,) = chainlinkWrapper.call{ value: bidAmount }(""); - if (!success) revert FailedToAllocateOEV(); - } - - // NOTE: Functions below are not delegatecalled - - // ---------------------------------------------------- // - // View Functions // - // ---------------------------------------------------- // - - function getBidFormat(UserOperation calldata) public pure override returns (address bidToken) { - return address(0); // ETH is bid token - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } - - function getSignersForBaseFeed(address baseChainlinkFeed) external view returns (address[] memory) { - return verificationVars[baseChainlinkFeed].signers; - } - - function getOracleDataForBaseFeed( - address baseChainlinkFeed, - address signer - ) - external - view - returns (Oracle memory) - { - return verificationVars[baseChainlinkFeed].oracles[signer]; - } - - // ---------------------------------------------------- // - // ChainlinkWrapper Factory Functions // - // ---------------------------------------------------- // - - // Creates a new wrapper contract for a specific Chainlink feed, to attribute OEV captured by Atlas to the - // OEV-generating protocol. - function createNewChainlinkAtlasWrapper(address baseChainlinkFeed) external returns (address) { - if (IChainlinkFeed(baseChainlinkFeed).latestAnswer() == 0) revert InvalidBaseFeed(); - address newWrapper = address(new ChainlinkAtlasWrapper(ATLAS, baseChainlinkFeed, msg.sender)); - isChainlinkWrapper[newWrapper] = true; - emit NewChainlinkWrapperCreated(newWrapper, baseChainlinkFeed, msg.sender); - return newWrapper; - } - - // Called by a ChainlinkAtlasWrapper to verify if the signers of a price update via `transmit` are verified. - function verifyTransmitSigners( - address baseChainlinkFeed, - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - external - view - returns (bool verified) - { - bool[] memory signed = new bool[](MAX_NUM_ORACLES); - bytes32 reportHash = keccak256(report); - uint256 observations = rs.length; - - VerificationVars storage verificationVar = verificationVars[baseChainlinkFeed]; - Oracle memory currentOracle; - - // Check number of observations is enough to reach quorum, but not more than max allowed otherwise will cause - // array out-of-bounds error - if (observations < observationsQuorum || observations > MAX_NUM_ORACLES) return false; - - for (uint256 i; i < observations; ++i) { - (address signer,,) = ECDSA.tryRecover(reportHash, uint8(rawVs[i]) + 27, rs[i], ss[i]); - currentOracle = verificationVar.oracles[signer]; - - // Signer must be pre-approved and only 1 observation per signer - if (currentOracle.role != Role.Signer || signed[currentOracle.index]) { - return false; - } - signed[currentOracle.index] = true; - } - return true; - } - - // ---------------------------------------------------- // - // OnlyGov Functions // - // ---------------------------------------------------- // - - function setObservationsQuorum(uint256 newQuorum) external onlyGov { - uint256 oldQuorum = observationsQuorum; - observationsQuorum = newQuorum; - emit ObservationsQuorumSet(oldQuorum, newQuorum); - } - - // Clears any existing signers and adds a new set of signers for a specific Chainlink feed. - function setSignersForBaseFeed(address baseChainlinkFeed, address[] calldata signers) external onlyGov { - if (signers.length > MAX_NUM_ORACLES) revert TooManySigners(); - - _removeAllSignersOfBaseFeed(baseChainlinkFeed); // Removes any existing signers first - VerificationVars storage vars = verificationVars[baseChainlinkFeed]; - - uint256 signersLength = signers.length; - for (uint256 i; i < signersLength; ++i) { - if (vars.oracles[signers[i]].role != Role.Unset) revert DuplicateSigner(signers[i]); - vars.oracles[signers[i]] = Oracle({ index: uint8(i), role: Role.Signer }); - } - vars.signers = signers; - - emit SignersSetForBaseFeed(baseChainlinkFeed, signers); - } - - // Adds a specific signer to a specific Chainlink feed. - function addSignerForBaseFeed(address baseChainlinkFeed, address signer) external onlyGov { - VerificationVars storage vars = verificationVars[baseChainlinkFeed]; - - if (vars.signers.length >= MAX_NUM_ORACLES) revert TooManySigners(); - if (vars.oracles[signer].role != Role.Unset) revert DuplicateSigner(signer); - - vars.signers.push(signer); - vars.oracles[signer] = Oracle({ index: uint8(vars.signers.length - 1), role: Role.Signer }); - - emit SignerAddedForBaseFeed(baseChainlinkFeed, signer); - } - - // Removes a specific signer from a specific Chainlink feed. - function removeSignerOfBaseFeed(address baseChainlinkFeed, address signer) external onlyGov { - Oracle memory oracle = verificationVars[baseChainlinkFeed].oracles[signer]; - address[] storage signers = verificationVars[baseChainlinkFeed].signers; - - if (oracle.role != Role.Signer) revert SignerNotFound(); - - if (oracle.index < signers.length - 1) { - address lastSigner = signers[signers.length - 1]; - signers[oracle.index] = lastSigner; - verificationVars[baseChainlinkFeed].oracles[lastSigner].index = oracle.index; - } - signers.pop(); - delete verificationVars[baseChainlinkFeed].oracles[signer]; - - emit SignerRemovedForBaseFeed(baseChainlinkFeed, signer); - } - - function _removeAllSignersOfBaseFeed(address baseChainlinkFeed) internal { - VerificationVars storage vars = verificationVars[baseChainlinkFeed]; - address[] storage signers = vars.signers; - uint256 signersLength = signers.length; - if (signersLength == 0) return; - for (uint256 i; i < signersLength; ++i) { - delete vars.oracles[signers[i]]; - } - delete vars.signers; - } - - function _onlyGov() internal view { - if (msg.sender != governance) revert OnlyGovernance(); - } - - modifier onlyGov() { - _onlyGov(); - _; - } -} - -interface IChainlinkAtlasWrapper { - function transmit(bytes calldata report, bytes32[] calldata rs, bytes32[] calldata ss, bytes32 rawVs) external; -} - -interface IChainlinkFeed { - function latestAnswer() external view returns (int256); -} diff --git a/src/contracts/examples/oev-example/ChainlinkDAppControlAlt.sol b/src/contracts/examples/oev-example/ChainlinkDAppControlAlt.sol deleted file mode 100644 index d518c4eb4..000000000 --- a/src/contracts/examples/oev-example/ChainlinkDAppControlAlt.sol +++ /dev/null @@ -1,239 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Atlas DApp-Control Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { ChainlinkAtlasWrapper } from "src/contracts/examples/oev-example/ChainlinkAtlasWrapperAlt.sol"; - -// Role enum as per Chainlink's OffchainAggregatorBilling.sol contract -enum Role { - // No oracle role has been set for address a - Unset, - // Signing address a of an oracle. I.e. report signatures from this oracle should ecrecover back to address a. - Signer, - // Transmitter role is not used - Transmitter -} - -struct Oracle { - uint8 index; // Index of oracle in signers array - Role role; // Role of the address which mapped to this struct -} - -struct VerificationVars { - mapping(address signer => Oracle oracle) oracles; - address[] signers; -} - -// NOTE: This contract acts as the Chainlink DAppControl for Atlas, -// and as a factory for ChainlinkAtlasWrapper contracts -contract ChainlinkDAppControl is DAppControl { - uint256 public constant MAX_NUM_ORACLES = 31; - - mapping(address baseChainlinkFeed => VerificationVars) internal verificationVars; - - error InvalidBaseFeed(); - error FailedToAllocateOEV(); - error OnlyGovernance(); - error SignerNotFound(); - error TooManySigners(); - error DuplicateSigner(address signer); - - event NewChainlinkWrapperCreated(address indexed wrapper, address indexed baseFeed, address indexed owner); - event SignersSetForBaseFeed(address indexed baseFeed, address[] signers); - event SignerAddedForBaseFeed(address indexed baseFeed, address indexed signer); - event SignerRemovedForBaseFeed(address indexed baseFeed, address indexed signer); - - constructor(address _atlas) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: false, - trackPreOpsReturnData: false, - trackUserReturnData: true, - delegateUser: false, - requirePreSolver: false, - requirePostSolver: false, - requirePostOps: false, - zeroSolvers: false, - reuseUserOp: false, - userAuctioneer: true, - solverAuctioneer: false, - unknownAuctioneer: true, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: false, // Update oracle even if all solvers fail - trustedOpHash: true, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { } - - // ---------------------------------------------------- // - // Atlas Hook Overrides // - // ---------------------------------------------------- // - - function _allocateValueCall(address, uint256 bidAmount, bytes calldata data) internal virtual override { - address chainlinkWrapper = abi.decode(data, (address)); - (bool success,) = chainlinkWrapper.call{ value: bidAmount }(""); - if (!success) revert FailedToAllocateOEV(); - } - - // NOTE: Functions below are not delegatecalled - - // ---------------------------------------------------- // - // View Functions // - // ---------------------------------------------------- // - - function getBidFormat(UserOperation calldata) public pure override returns (address bidToken) { - return address(0); // ETH is bid token - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } - - function getSignersForBaseFeed(address baseChainlinkFeed) external view returns (address[] memory) { - return verificationVars[baseChainlinkFeed].signers; - } - - function getOracleDataForBaseFeed( - address baseChainlinkFeed, - address signer - ) - external - view - returns (Oracle memory) - { - return verificationVars[baseChainlinkFeed].oracles[signer]; - } - - // ---------------------------------------------------- // - // ChainlinkWrapper Factory Functions // - // ---------------------------------------------------- // - - // Creates a new wrapper contract for a specific Chainlink feed, to attribute OEV captured by Atlas to the - // OEV-generating protocol. - function createNewChainlinkAtlasWrapper(address baseChainlinkFeed) external returns (address) { - if (IChainlinkFeed(baseChainlinkFeed).latestAnswer() == 0) revert InvalidBaseFeed(); - address newWrapper = address(new ChainlinkAtlasWrapper(ATLAS, baseChainlinkFeed, msg.sender)); - emit NewChainlinkWrapperCreated(newWrapper, baseChainlinkFeed, msg.sender); - return newWrapper; - } - - // Called by a ChainlinkAtlasWrapper to verify if the signers of a price update via `transmit` are verified. - function verifyTransmitSigners( - address baseChainlinkFeed, - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - external - view - returns (bool verified) - { - bool[] memory signed = new bool[](MAX_NUM_ORACLES); - bytes32 reportHash = keccak256(report); - Oracle memory currentOracle; - - for (uint256 i; i < rs.length; ++i) { - address signer = ecrecover(reportHash, uint8(rawVs[i]) + 27, rs[i], ss[i]); - currentOracle = verificationVars[baseChainlinkFeed].oracles[signer]; - - // Signer must be pre-approved and only 1 observation per signer - if (currentOracle.role != Role.Signer || signed[currentOracle.index]) { - return false; - } - signed[currentOracle.index] = true; - } - return true; - } - - // ---------------------------------------------------- // - // OnlyGov Functions // - // ---------------------------------------------------- // - - // Clears any existing signers and adds a new set of signers for a specific Chainlink feed. - function setSignersForBaseFeed(address baseChainlinkFeed, address[] calldata signers) external onlyGov { - if (signers.length > MAX_NUM_ORACLES) revert TooManySigners(); - - _removeAllSignersOfBaseFeed(baseChainlinkFeed); // Removes any existing signers first - VerificationVars storage vars = verificationVars[baseChainlinkFeed]; - - for (uint256 i; i < signers.length; ++i) { - if (vars.oracles[signers[i]].role != Role.Unset) revert DuplicateSigner(signers[i]); - vars.oracles[signers[i]] = Oracle({ index: uint8(i), role: Role.Signer }); - } - vars.signers = signers; - - emit SignersSetForBaseFeed(baseChainlinkFeed, signers); - } - - // Adds a specific signer to a specific Chainlink feed. - function addSignerForBaseFeed(address baseChainlinkFeed, address signer) external onlyGov { - VerificationVars storage vars = verificationVars[baseChainlinkFeed]; - - if (vars.signers.length >= MAX_NUM_ORACLES) revert TooManySigners(); - if (vars.oracles[signer].role != Role.Unset) revert DuplicateSigner(signer); - - vars.signers.push(signer); - vars.oracles[signer] = Oracle({ index: uint8(vars.signers.length - 1), role: Role.Signer }); - - emit SignerAddedForBaseFeed(baseChainlinkFeed, signer); - } - - // Removes a specific signer from a specific Chainlink feed. - function removeSignerOfBaseFeed(address baseChainlinkFeed, address signer) external onlyGov { - Oracle memory oracle = verificationVars[baseChainlinkFeed].oracles[signer]; - address[] storage signers = verificationVars[baseChainlinkFeed].signers; - - if (oracle.role != Role.Signer) revert SignerNotFound(); - - if (oracle.index < signers.length - 1) { - signers[oracle.index] = signers[signers.length - 1]; - verificationVars[baseChainlinkFeed].oracles[signers[oracle.index]].index = oracle.index; - } - signers.pop(); - delete verificationVars[baseChainlinkFeed].oracles[signer]; - - emit SignerRemovedForBaseFeed(baseChainlinkFeed, signer); - } - - function _removeAllSignersOfBaseFeed(address baseChainlinkFeed) internal { - VerificationVars storage vars = verificationVars[baseChainlinkFeed]; - address[] storage signers = vars.signers; - if (signers.length == 0) return; - for (uint256 i; i < signers.length; ++i) { - delete vars.oracles[signers[i]]; - } - delete vars.signers; - } - - function _onlyGov() internal view { - if (msg.sender != governance) revert OnlyGovernance(); - } - - modifier onlyGov() { - _onlyGov(); - _; - } -} - -interface IChainlinkAtlasWrapper { - function transmit(bytes calldata report, bytes32[] calldata rs, bytes32[] calldata ss, bytes32 rawVs) external; -} - -interface IChainlinkFeed { - function latestAnswer() external view returns (int256); -} diff --git a/src/contracts/examples/oev-example/IChainlinkAtlasWrapper.sol b/src/contracts/examples/oev-example/IChainlinkAtlasWrapper.sol deleted file mode 100644 index a45b6e7b8..000000000 --- a/src/contracts/examples/oev-example/IChainlinkAtlasWrapper.sol +++ /dev/null @@ -1,50 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -import { IChainlinkDAppControl } from "./IChainlinkDAppControl.sol"; - -interface AggregatorInterface { - function latestAnswer() external view returns (int256); - function latestTimestamp() external view returns (uint256); - function latestRound() external view returns (uint256); - function getAnswer(uint256 roundId) external view returns (int256); - function getTimestamp(uint256 roundId) external view returns (uint256); - - event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); - event NewRound(uint256 indexed roundId, address indexed startedBy, uint256 startedAt); -} - -interface AggregatorV3Interface { - function decimals() external view returns (uint8); - function description() external view returns (string memory); - function version() external view returns (uint256); - - // getRoundData and latestRoundData should both raise "No data present" - // if they do not have data to report, instead of returning unset values - // which could be misinterpreted as actual reported values. - function getRoundData(uint80 _roundId) - external - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); - function latestRoundData() - external - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); -} - -interface AggregatorV2V3Interface is AggregatorInterface, AggregatorV3Interface { } - -interface IChainlinkAtlasWrapper is AggregatorV2V3Interface { - function transmit( - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - external - returns (address); - - function setTransmitterStatus(address transmitter, bool trusted) external; - function withdrawETH(address recipient) external; - function withdrawERC20(address token, address recipient) external; -} diff --git a/src/contracts/examples/oev-example/IChainlinkDAppControl.sol b/src/contracts/examples/oev-example/IChainlinkDAppControl.sol deleted file mode 100644 index 3c0ff8944..000000000 --- a/src/contracts/examples/oev-example/IChainlinkDAppControl.sol +++ /dev/null @@ -1,15 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -interface IChainlinkDAppControl { - function verifyTransmitSigners( - address baseChainlinkFeed, - bytes calldata report, - bytes32[] calldata rs, - bytes32[] calldata ss, - bytes32 rawVs - ) - external - view - returns (bool verified); -} diff --git a/src/contracts/examples/trebleswap/TrebleSwapDAppControl.sol b/src/contracts/examples/trebleswap/TrebleSwapDAppControl.sol deleted file mode 100644 index c0d06f083..000000000 --- a/src/contracts/examples/trebleswap/TrebleSwapDAppControl.sol +++ /dev/null @@ -1,230 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; - -struct SwapTokenInfo { - address inputToken; - uint256 inputAmount; - address outputToken; - uint256 outputMin; -} - -contract TrebleSwapDAppControl is DAppControl { - address public constant ODOS_ROUTER = 0x19cEeAd7105607Cd444F5ad10dd51356436095a1; - - // TODO TREB token not available yet - replace when it is. DEGEN address for now. - address public constant TREB = 0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed; - address internal constant _ETH = address(0); - address internal constant _BURN = address(0xdead); - - error InvalidUserOpData(); - error UserOpDappNotOdosRouter(); - error InsufficientUserOpValue(); - error InsufficientTrebBalance(); - error InsufficientOutputBalance(); - - constructor(address atlas) - DAppControl( - atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: true, - trackPreOpsReturnData: true, - trackUserReturnData: false, - delegateUser: false, - requirePreSolver: false, - requirePostSolver: false, - requirePostOps: true, - zeroSolvers: true, - reuseUserOp: true, - userAuctioneer: true, - solverAuctioneer: false, - unknownAuctioneer: false, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: false, - trustedOpHash: false, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { } - - // ---------------------------------------------------- // - // ATLAS HOOKS // - // ---------------------------------------------------- // - - function _preOpsCall(UserOperation calldata userOp) internal virtual override returns (bytes memory) { - if (userOp.dapp != ODOS_ROUTER) revert UserOpDappNotOdosRouter(); - - (bool success, bytes memory swapData) = - CONTROL.staticcall(abi.encodePacked(this.decodeUserOpData.selector, userOp.data)); - - if (!success) revert InvalidUserOpData(); - - SwapTokenInfo memory _swapInfo = abi.decode(swapData, (SwapTokenInfo)); - - // If inputToken is ERC20, transfer tokens from user to EE, and approve Odos router for swap - if (_swapInfo.inputToken != _ETH) { - _transferUserERC20(_swapInfo.inputToken, address(this), _swapInfo.inputAmount); - SafeTransferLib.safeApprove(_swapInfo.inputToken, ODOS_ROUTER, _swapInfo.inputAmount); - } else { - if (userOp.value < _swapInfo.inputAmount) revert InsufficientUserOpValue(); - } - - return swapData; // return SwapTokenInfo in bytes format, to be used in allocateValue. - } - - function _allocateValueCall(address, uint256 bidAmount, bytes calldata data) internal virtual override { - SwapTokenInfo memory _swapInfo = abi.decode(data, (SwapTokenInfo)); - uint256 _outputTokenBalance = _balanceOf(_swapInfo.outputToken); - uint256 _inputTokenBalance = _balanceOf(_swapInfo.inputToken); - - if (_outputTokenBalance < _swapInfo.outputMin) revert InsufficientOutputBalance(); - - // Burn TREB bid - SafeTransferLib.safeTransfer(TREB, _BURN, bidAmount); - - _transferUserTokens(_swapInfo, _outputTokenBalance, _inputTokenBalance); - } - - function _postOpsCall(bool solved, bytes calldata data) internal virtual override { - if (solved) return; // token distribution already handled in allocateValue hook - - SwapTokenInfo memory _swapInfo = abi.decode(data, (SwapTokenInfo)); - uint256 _outputTokenBalance = _balanceOf(_swapInfo.outputToken); - uint256 _inputTokenBalance = _balanceOf(_swapInfo.inputToken); - - if (_outputTokenBalance < _swapInfo.outputMin) revert InsufficientOutputBalance(); - - _transferUserTokens(_swapInfo, _outputTokenBalance, _inputTokenBalance); - } - - // ---------------------------------------------------- // - // GETTERS AND HELPERS // - // ---------------------------------------------------- // - - function getBidFormat(UserOperation calldata) public view virtual override returns (address bidToken) { - return TREB; - } - - function getBidValue(SolverOperation calldata solverOp) public view virtual override returns (uint256) { - return solverOp.bidAmount; - } - - function _transferUserTokens( - SwapTokenInfo memory swapInfo, - uint256 outputTokenBalance, - uint256 inputTokenBalance - ) - internal - { - // Transfer output token to user - if (swapInfo.outputToken == _ETH) { - SafeTransferLib.safeTransferETH(_user(), outputTokenBalance); - } else { - SafeTransferLib.safeTransfer(swapInfo.outputToken, _user(), outputTokenBalance); - } - - // If any leftover input token, transfer back to user - if (inputTokenBalance > 0) { - if (swapInfo.inputToken == _ETH) { - SafeTransferLib.safeTransferETH(_user(), inputTokenBalance); - } else { - SafeTransferLib.safeTransfer(swapInfo.inputToken, _user(), inputTokenBalance); - } - } - } - - // Call this helper with userOp.data appended as calldata, to decode swap info in the Odos calldata. - function decodeUserOpData() public view returns (SwapTokenInfo memory swapTokenInfo) { - assembly { - // helper function to get address either from calldata or Odos router addressList() - function getAddress(currPos) -> result, newPos { - let inputPos := shr(240, calldataload(currPos)) - - switch inputPos - // Reserve the null address as a special case that can be specified with 2 null bytes - case 0x0000 { newPos := add(currPos, 2) } - // This case means that the address is encoded in the calldata directly following the code - case 0x0001 { - result := and(shr(80, calldataload(currPos)), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - newPos := add(currPos, 22) - } - // If not 0000 or 0001, call ODOS_ROUTER.addressList(inputPos - 2) to get address - default { - // 0000 and 0001 are reserved for cases above, so offset by 2 for addressList index - let arg := sub(inputPos, 2) - let selector := 0xb810fb43 // function selector for "addressList(uint256)" - let ptr := mload(0x40) // get the free memory pointer - mstore(ptr, shl(224, selector)) // shift selector to left of slot and store - mstore(add(ptr, 4), arg) // store the uint256 argument after the selector - - // Perform the external call - let success := - staticcall( - gas(), // gas remaining - ODOS_ROUTER, - ptr, // input location - 0x24, // input size (4 byte selector + uint256 arg) - ptr, // output location - 0x20 // output size (32 bytes for the address) - ) - - if eq(success, 0) { revert(0, 0) } - - result := mload(ptr) - newPos := add(currPos, 2) - } - } - - let result := 0 - let pos := 8 // skip Odos compactSwap selector and this helper selector (4 + 4 bytes) - - // swapTokenInfo.inputToken (slot 0) - result, pos := getAddress(pos) - mstore(swapTokenInfo, result) - - // swapTokenInfo.outputToken (slot 2) - result, pos := getAddress(pos) - mstore(add(swapTokenInfo, 0x40), result) - - // swapTokenInfo.inputAmount (slot 1) - let inputAmountLength := shr(248, calldataload(pos)) - pos := add(pos, 1) - if inputAmountLength { - mstore(add(swapTokenInfo, 0x20), shr(mul(sub(32, inputAmountLength), 8), calldataload(pos))) - pos := add(pos, inputAmountLength) - } - - // swapTokenInfo.outputMin (slot 3) - // get outputQuote and slippageTolerance from calldata, then calculate outputMin - let quoteAmountLength := shr(248, calldataload(pos)) - pos := add(pos, 1) - let outputQuote := shr(mul(sub(32, quoteAmountLength), 8), calldataload(pos)) - pos := add(pos, quoteAmountLength) - { - let slippageTolerance := shr(232, calldataload(pos)) - mstore(add(swapTokenInfo, 0x60), div(mul(outputQuote, sub(0xFFFFFF, slippageTolerance)), 0xFFFFFF)) - } - } - } - - function _balanceOf(address token) internal view returns (uint256) { - if (token == _ETH) { - return address(this).balance; - } else { - return SafeTransferLib.balanceOf(token, address(this)); - } - } -} diff --git a/src/contracts/examples/v2-example-router/V2RewardDAppControl.sol b/src/contracts/examples/v2-example-router/V2RewardDAppControl.sol deleted file mode 100644 index eae49ece4..000000000 --- a/src/contracts/examples/v2-example-router/V2RewardDAppControl.sol +++ /dev/null @@ -1,218 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; - -// Uniswap Imports -import { IUniswapV2Router01, IUniswapV2Router02 } from "./interfaces/IUniswapV2Router.sol"; - -/* -* @title V2RewardDAppControl -* @notice This contract is a Uniswap v2 "backrun" module that rewards users with an arbitrary ERC20 token (or ETH) for - MEV generating swaps conducted on a UniswapV2Router02. The bid amount paid by solvers (the "reward token") is gifted - to users. -* @notice Frontends can easily offer gasless swaps to users selling ERC20 tokens (users would need to approve Atlas to - spend their tokens first). For ETH swaps, the user would need to bundle their own operation. -* @notice The reward token can be ETH (address(0)) or any ERC20 token. Solvers are required to pay their bid with that - token. */ -contract V2RewardDAppControl is DAppControl { - address public immutable REWARD_TOKEN; - address public immutable uniswapV2Router02; - - mapping(bytes4 => bool) public ERC20StartingSelectors; - mapping(bytes4 => bool) public ETHStartingSelectors; - mapping(bytes4 => bool) public exactINSelectors; - - event TokensRewarded(address indexed user, address indexed token, uint256 amount); - - constructor( - address _atlas, - address _rewardToken, - address _uniswapV2Router02 - ) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: true, - trackPreOpsReturnData: true, - trackUserReturnData: false, - delegateUser: false, - requirePreSolver: false, - requirePostSolver: false, - requirePostOps: true, - zeroSolvers: true, - reuseUserOp: false, - userAuctioneer: true, - solverAuctioneer: false, - unknownAuctioneer: true, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: false, - trustedOpHash: true, - invertBidValue: false, - exPostBids: true, - allowAllocateValueFailure: false - }) - ) - { - REWARD_TOKEN = _rewardToken; - uniswapV2Router02 = _uniswapV2Router02; - - ERC20StartingSelectors[bytes4(IUniswapV2Router01.swapExactTokensForTokens.selector)] = true; - ERC20StartingSelectors[bytes4(IUniswapV2Router01.swapTokensForExactTokens.selector)] = true; - ERC20StartingSelectors[bytes4(IUniswapV2Router01.swapTokensForExactETH.selector)] = true; - ERC20StartingSelectors[bytes4(IUniswapV2Router01.swapExactTokensForETH.selector)] = true; - ERC20StartingSelectors[bytes4(IUniswapV2Router02.swapExactTokensForTokensSupportingFeeOnTransferTokens.selector)] - = true; - ERC20StartingSelectors[bytes4(IUniswapV2Router02.swapExactTokensForETHSupportingFeeOnTransferTokens.selector)] = - true; - - ETHStartingSelectors[bytes4(IUniswapV2Router01.swapExactETHForTokens.selector)] = true; - ETHStartingSelectors[bytes4(IUniswapV2Router01.swapETHForExactTokens.selector)] = true; - ETHStartingSelectors[bytes4(IUniswapV2Router02.swapExactETHForTokensSupportingFeeOnTransferTokens.selector)] = - true; - - exactINSelectors[bytes4(IUniswapV2Router01.swapExactTokensForTokens.selector)] = true; - exactINSelectors[bytes4(IUniswapV2Router01.swapExactTokensForETH.selector)] = true; - exactINSelectors[bytes4(IUniswapV2Router02.swapExactTokensForTokensSupportingFeeOnTransferTokens.selector)] = - true; - exactINSelectors[bytes4(IUniswapV2Router02.swapExactTokensForETHSupportingFeeOnTransferTokens.selector)] = true; - exactINSelectors[bytes4(IUniswapV2Router01.swapExactETHForTokens.selector)] = true; - exactINSelectors[bytes4(IUniswapV2Router02.swapExactETHForTokensSupportingFeeOnTransferTokens.selector)] = true; - } - - // ---------------------------------------------------- // - // Custom // - // ---------------------------------------------------- // - - /* - * @notice This function inspects the user's call data to determine the token they are selling and the amount sold - * @param userData The user's call data - * @return tokenSold The address of the ERC20 token the user is selling (or address(0) for ETH) - * @return amountSold The amount of the token sold - */ - function getTokenSold(bytes calldata userData) external view returns (address tokenSold, uint256 amountSold) { - bytes4 funcSelector = bytes4(userData); - - // User is only allowed to call swap functions - require( - ERC20StartingSelectors[funcSelector] || ETHStartingSelectors[funcSelector], - "V2RewardDAppControl: InvalidFunction" - ); - - if (ERC20StartingSelectors[funcSelector]) { - address[] memory path; - - if (exactINSelectors[funcSelector]) { - // Exact amount sold - (amountSold,, path,,) = abi.decode(userData[4:], (uint256, uint256, address[], address, uint256)); - } else { - // Max amount sold, unused amount will be refunded in the _postOpsCall hook if any - (, amountSold, path,,) = abi.decode(userData[4:], (uint256, uint256, address[], address, uint256)); - } - - tokenSold = path[0]; - } - } - - // ---------------------------------------------------- // - // Atlas hooks // - // ---------------------------------------------------- // - - function _checkUserOperation(UserOperation memory userOp) internal view override { - // User is only allowed to call UniswapV2Router02 - require(userOp.dapp == uniswapV2Router02, "V2RewardDAppControl: InvalidDestination"); - } - - /* - * @notice This function is called before the user's call to UniswapV2Router02 - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev If the user is selling an ERC20 token, the function transfers the tokens from the user to the - ExecutionEnvironment and approves UniswapV2Router02 to spend the tokens from the ExecutionEnvironment - * @param userOp The UserOperation struct - * @return The address of the ERC20 token the user is selling (or address(0) for ETH), which is used in the - _postOpsCall hook to refund leftover dust, if any - */ - function _preOpsCall(UserOperation calldata userOp) internal override returns (bytes memory) { - // The current hook is delegatecalled, so we need to call the userOp.control to access the mappings - (address tokenSold, uint256 amountSold) = V2RewardDAppControl(userOp.control).getTokenSold(userOp.data); - - // Pull the tokens from the user and approve UniswapV2Router02 to spend them - _getAndApproveUserERC20(tokenSold, amountSold, uniswapV2Router02); - - // Return tokenSold for the _postOpsCall hook to be able to refund dust - return abi.encode(tokenSold); - } - - /* - * @notice This function is called after a solver has successfully paid their bid - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It simply transfers the reward token to the user (solvers are required to pay their bid with the reward - token, so we don't have any more steps to take here) - * @param bidToken The address of the token used for the winning SolverOperation's bid - * @param bidAmount The winning bid amount - * @param _ - */ - function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata) internal override { - require(bidToken == REWARD_TOKEN, "V2RewardDAppControl: InvalidBidToken"); - - address user = _user(); - - if (bidToken == address(0)) { - SafeTransferLib.safeTransferETH(user, bidAmount); - } else { - SafeTransferLib.safeTransfer(REWARD_TOKEN, user, bidAmount); - } - - emit TokensRewarded(user, REWARD_TOKEN, bidAmount); - } - - /* - * @notice This function is called as the last phase of a `metacall` transaction - * @dev This function is delegatecalled: msg.sender = Atlas, address(this) = ExecutionEnvironment - * @dev It refunds any leftover dust (ETH/ERC20) to the user (this can occur when the user is calling an exactOUT - function and the amount sold is less than the amountInMax) - * @param data The address of the ERC20 token the user is selling (or address(0) for ETH), that was returned by the - _preOpsCall hook - */ - function _postOpsCall(bool, bytes calldata data) internal override { - address tokenSold = abi.decode(data, (address)); - uint256 balance; - - // Refund ETH/ERC20 dust if any - if (tokenSold == address(0)) { - balance = address(this).balance; - if (balance > 0) { - SafeTransferLib.safeTransferETH(_user(), balance); - } - } else { - balance = IERC20(tokenSold).balanceOf(address(this)); - if (balance > 0) { - SafeTransferLib.safeTransfer(tokenSold, _user(), balance); - } - } - } - - // ---------------------------------------------------- // - // Getters and helpers // - // ---------------------------------------------------- // - - function getBidFormat(UserOperation calldata) public view override returns (address bidToken) { - return REWARD_TOKEN; - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } -} diff --git a/src/contracts/examples/v2-example-router/interfaces/IUniswapV2Router.sol b/src/contracts/examples/v2-example-router/interfaces/IUniswapV2Router.sol deleted file mode 100644 index a64c4e7bc..000000000 --- a/src/contracts/examples/v2-example-router/interfaces/IUniswapV2Router.sol +++ /dev/null @@ -1,219 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.5.0; - -interface IUniswapV2Router01 { - function factory() external pure returns (address); - function WETH() external pure returns (address); - - function addLiquidity( - address tokenA, - address tokenB, - uint256 amountADesired, - uint256 amountBDesired, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline - ) - external - returns (uint256 amountA, uint256 amountB, uint256 liquidity); - function addLiquidityETH( - address token, - uint256 amountTokenDesired, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) - external - payable - returns (uint256 amountToken, uint256 amountETH, uint256 liquidity); - function removeLiquidity( - address tokenA, - address tokenB, - uint256 liquidity, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline - ) - external - returns (uint256 amountA, uint256 amountB); - function removeLiquidityETH( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) - external - returns (uint256 amountToken, uint256 amountETH); - function removeLiquidityWithPermit( - address tokenA, - address tokenB, - uint256 liquidity, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) - external - returns (uint256 amountA, uint256 amountB); - function removeLiquidityETHWithPermit( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) - external - returns (uint256 amountToken, uint256 amountETH); - function swapExactTokensForTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external - returns (uint256[] memory amounts); - function swapTokensForExactTokens( - uint256 amountOut, - uint256 amountInMax, - address[] calldata path, - address to, - uint256 deadline - ) - external - returns (uint256[] memory amounts); - function swapExactETHForTokens( - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external - payable - returns (uint256[] memory amounts); - function swapTokensForExactETH( - uint256 amountOut, - uint256 amountInMax, - address[] calldata path, - address to, - uint256 deadline - ) - external - returns (uint256[] memory amounts); - function swapExactTokensForETH( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external - returns (uint256[] memory amounts); - function swapETHForExactTokens( - uint256 amountOut, - address[] calldata path, - address to, - uint256 deadline - ) - external - payable - returns (uint256[] memory amounts); - - function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external pure returns (uint256 amountB); - function getAmountOut( - uint256 amountIn, - uint256 reserveIn, - uint256 reserveOut - ) - external - pure - returns (uint256 amountOut); - function getAmountIn( - uint256 amountOut, - uint256 reserveIn, - uint256 reserveOut - ) - external - pure - returns (uint256 amountIn); - function getAmountsOut( - uint256 amountIn, - address[] calldata path - ) - external - view - returns (uint256[] memory amounts); - function getAmountsIn( - uint256 amountOut, - address[] calldata path - ) - external - view - returns (uint256[] memory amounts); -} - -interface IUniswapV2Router02 is IUniswapV2Router01 { - function removeLiquidityETHSupportingFeeOnTransferTokens( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) - external - returns (uint256 amountETH); - function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) - external - returns (uint256 amountETH); - - function swapExactTokensForTokensSupportingFeeOnTransferTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external; - function swapExactETHForTokensSupportingFeeOnTransferTokens( - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external - payable; - function swapExactTokensForETHSupportingFeeOnTransferTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external; -} diff --git a/src/contracts/examples/v2-example/SwapMath.sol b/src/contracts/examples/v2-example/SwapMath.sol deleted file mode 100644 index e58c917cb..000000000 --- a/src/contracts/examples/v2-example/SwapMath.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -library SwapMath { - function getAmountIn( - uint256 amountOut, - uint256 reservesIn, - uint256 reservesOut - ) - internal - pure - returns (uint256 amountIn) - { - uint256 numerator = reservesIn * amountOut * 1000; - uint256 denominator = (reservesOut - amountOut) * 997; - amountIn = (numerator / denominator) + 1; - } - - function getAmountOut( - uint256 amountIn, - uint256 reserveIn, - uint256 reserveOut - ) - internal - pure - returns (uint256 amountOut) - { - uint256 amountInWithFee = amountIn * 997; - uint256 numerator = amountInWithFee * reserveOut; - uint256 denominator = (reserveIn * 1000) + amountInWithFee; - amountOut = numerator / denominator; - } -} diff --git a/src/contracts/examples/v2-example/V2DAppControl.sol b/src/contracts/examples/v2-example/V2DAppControl.sol deleted file mode 100644 index 9eef3516a..000000000 --- a/src/contracts/examples/v2-example/V2DAppControl.sol +++ /dev/null @@ -1,181 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; - -// Atlas Base Imports -import { IExecutionEnvironment } from "src/contracts/interfaces/IExecutionEnvironment.sol"; - -import { SafetyBits } from "src/contracts/libraries/SafetyBits.sol"; - -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Atlas DApp-Control Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; - -// Uni V2 Imports -import { IUniswapV2Pair } from "./interfaces/IUniswapV2Pair.sol"; -import { IUniswapV2Factory } from "./interfaces/IUniswapV2Factory.sol"; - -// Misc -import { SwapMath } from "./SwapMath.sol"; - -// import "forge-std/Test.sol"; - -interface IWETH { - function deposit() external payable; - function withdraw(uint256 wad) external; -} - -// A DAppControl that for Uniswap V2 style swaps -// User call should be made to Uniswap V2 pair contracts (not router) -// WARNING : Offers no slippage protection, so not recommended for production use. -// For slippage protection, use V2RewardDAppControl in which user calls are made to router. -contract V2DAppControl is DAppControl { - uint256 public constant CONTROL_GAS_USAGE = 250_000; - - address public constant WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - address public constant GOVERNANCE_TOKEN = address(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); - address public constant WETH_X_GOVERNANCE_POOL = address(0xd3d2E2692501A5c9Ca623199D38826e513033a17); - - address public constant BURN_ADDRESS = - address(uint160(uint256(keccak256(abi.encodePacked("GOVERNANCE TOKEN BURN ADDRESS"))))); - - bytes4 public constant SWAP = bytes4(IUniswapV2Pair.swap.selector); - - bool public immutable govIsTok0; - - event GiftedGovernanceToken(address indexed user, address indexed token, uint256 amount); - - constructor(address _atlas) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: true, - trackPreOpsReturnData: false, - trackUserReturnData: false, - delegateUser: false, - requirePreSolver: false, - requirePostSolver: false, - requirePostOps: false, - zeroSolvers: true, - reuseUserOp: false, - userAuctioneer: true, - solverAuctioneer: true, - unknownAuctioneer: true, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: false, - trustedOpHash: false, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { - govIsTok0 = (IUniswapV2Pair(WETH_X_GOVERNANCE_POOL).token0() == GOVERNANCE_TOKEN); - if (govIsTok0) { - require(IUniswapV2Pair(WETH_X_GOVERNANCE_POOL).token1() == WETH, "INVALID TOKEN PAIR"); - } else { - require(IUniswapV2Pair(WETH_X_GOVERNANCE_POOL).token0() == WETH, "INVALID TOKEN PAIR"); - require(IUniswapV2Pair(WETH_X_GOVERNANCE_POOL).token1() == GOVERNANCE_TOKEN, "INVALID TOKEN PAIR"); - } - } - - function _checkUserOperation(UserOperation memory userOp) internal view override { - require(bytes4(userOp.data) == SWAP, "ERR-H10 InvalidFunction"); - - require( - IUniswapV2Factory(IUniswapV2Pair(userOp.dapp).factory()).getPair( - IUniswapV2Pair(userOp.dapp).token0(), IUniswapV2Pair(userOp.dapp).token1() - ) == userOp.dapp, - "ERR-H11 Invalid pair" - ); - } - - function _preOpsCall(UserOperation calldata userOp) internal override returns (bytes memory) { - ( - uint256 amount0Out, - uint256 amount1Out, - , // address recipient // Unused - // bytes memory swapData // Unused - ) = abi.decode(userOp.data[4:], (uint256, uint256, address, bytes)); - - require(amount0Out == 0 || amount1Out == 0, "ERR-H12 InvalidAmountOuts"); - require(amount0Out > 0 || amount1Out > 0, "ERR-H13 InvalidAmountOuts"); - - (uint112 token0Balance, uint112 token1Balance,) = IUniswapV2Pair(userOp.dapp).getReserves(); - - uint256 amount0In = - amount1Out == 0 ? 0 : SwapMath.getAmountIn(amount1Out, uint256(token0Balance), uint256(token1Balance)); - uint256 amount1In = - amount0Out == 0 ? 0 : SwapMath.getAmountIn(amount0Out, uint256(token1Balance), uint256(token0Balance)); - - // This is a V2 swap, so optimistically transfer the tokens - // NOTE: The user should have approved the ExecutionEnvironment for token transfers - _transferUserERC20( - amount0Out > amount1Out ? IUniswapV2Pair(userOp.dapp).token1() : IUniswapV2Pair(userOp.dapp).token0(), - userOp.dapp, - amount0In > amount1In ? amount0In : amount1In - ); - - bytes memory emptyData; - return emptyData; - } - - // This occurs after a Solver has successfully paid their bid, which is - // held in ExecutionEnvironment. - function _allocateValueCall(address, uint256 bidAmount, bytes calldata) internal override { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Escrow - - address user = _user(); - - (uint112 token0Balance, uint112 token1Balance,) = IUniswapV2Pair(WETH_X_GOVERNANCE_POOL).getReserves(); - - SafeTransferLib.safeTransfer(WETH, WETH_X_GOVERNANCE_POOL, bidAmount); - - uint256 amount0Out; - uint256 amount1Out; - - if (govIsTok0) { - amount0Out = SwapMath.getAmountOut(bidAmount, uint256(token1Balance), uint256(token0Balance)); - } else { - amount1Out = SwapMath.getAmountOut(bidAmount, uint256(token0Balance), uint256(token1Balance)); - } - - bytes memory nullBytes; - IUniswapV2Pair(WETH_X_GOVERNANCE_POOL).swap(amount0Out, amount1Out, user, nullBytes); - - emit GiftedGovernanceToken(user, GOVERNANCE_TOKEN, govIsTok0 ? amount0Out : amount1Out); - - /* - // ENABLE FOR FOUNDRY TESTING - console.log("----====++++====----"); - console.log("DApp Control"); - console.log("Governance Tokens Sent to user:", govIsTok0 ? amount0Out : amount1Out); - console.log("----====++++====----"); - */ - } - - ///////////////// GETTERS & HELPERS // ////////////////// - - function getBidFormat(UserOperation calldata) public pure override returns (address bidToken) { - // This is a helper function called by solvers - // so that they can get the proper format for - // submitting their bids to the hook. - return WETH; - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } -} diff --git a/src/contracts/examples/v2-example/interfaces/IUniswapV2Factory.sol b/src/contracts/examples/v2-example/interfaces/IUniswapV2Factory.sol deleted file mode 100644 index d9e676e92..000000000 --- a/src/contracts/examples/v2-example/interfaces/IUniswapV2Factory.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.5.0; - -interface IUniswapV2Factory { - event PairCreated(address indexed token0, address indexed token1, address pair, uint256); - - function feeTo() external view returns (address); - function feeToSetter() external view returns (address); - - function getPair(address tokenA, address tokenB) external view returns (address pair); - function allPairs(uint256) external view returns (address pair); - function allPairsLength() external view returns (uint256); - - function createPair(address tokenA, address tokenB) external returns (address pair); - - function setFeeTo(address) external; - function setFeeToSetter(address) external; -} diff --git a/src/contracts/examples/v2-example/interfaces/IUniswapV2Pair.sol b/src/contracts/examples/v2-example/interfaces/IUniswapV2Pair.sol deleted file mode 100644 index 751eea813..000000000 --- a/src/contracts/examples/v2-example/interfaces/IUniswapV2Pair.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.5.0; - -interface IUniswapV2Pair { - event Approval(address indexed owner, address indexed spender, uint256 value); - event Transfer(address indexed from, address indexed to, uint256 value); - - function name() external pure returns (string memory); - function symbol() external pure returns (string memory); - function decimals() external pure returns (uint8); - function totalSupply() external view returns (uint256); - function balanceOf(address owner) external view returns (uint256); - function allowance(address owner, address spender) external view returns (uint256); - - function approve(address spender, uint256 value) external returns (bool); - function transfer(address to, uint256 value) external returns (bool); - function transferFrom(address from, address to, uint256 value) external returns (bool); - - function nonces(address owner) external view returns (uint256); - - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) - external; - - event Mint(address indexed sender, uint256 amount0, uint256 amount1); - event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); - event Swap( - address indexed sender, - uint256 amount0In, - uint256 amount1In, - uint256 amount0Out, - uint256 amount1Out, - address indexed to - ); - event Sync(uint112 reserve0, uint112 reserve1); - - function factory() external view returns (address); - function token0() external view returns (address); - function token1() external view returns (address); - function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); - function price0CumulativeLast() external view returns (uint256); - function price1CumulativeLast() external view returns (uint256); - function kLast() external view returns (uint256); - - function mint(address to) external returns (uint256 liquidity); - function burn(address to) external returns (uint256 amount0, uint256 amount1); - function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; - function skim(address to) external; - function sync() external; - - function initialize(address, address) external; -} diff --git a/src/contracts/examples/v4-example/IHooks.sol b/src/contracts/examples/v4-example/IHooks.sol deleted file mode 100644 index 3f4fa5caf..000000000 --- a/src/contracts/examples/v4-example/IHooks.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.25; - -import { IPoolManager } from "./IPoolManager.sol"; - -interface IHooks { - function beforeSwap( - address sender, - IPoolManager.PoolKey calldata key, - IPoolManager.SwapParams calldata params - ) - external - returns (bytes4); - - struct Calls { - bool beforeInitialize; - bool afterInitialize; - bool beforeModifyPosition; - bool afterModifyPosition; - bool beforeSwap; - bool afterSwap; - bool beforeDonate; - bool afterDonate; - } -} diff --git a/src/contracts/examples/v4-example/IPoolManager.sol b/src/contracts/examples/v4-example/IPoolManager.sol deleted file mode 100644 index a2e49b34e..000000000 --- a/src/contracts/examples/v4-example/IPoolManager.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.25; - -import { IHooks } from "./IHooks.sol"; - -interface IPoolManager { - type Currency is address; - type BalanceDelta is int256; - - struct SwapParams { - bool zeroForOne; - int256 amountSpecified; - uint160 sqrtPriceLimitX96; - } - - struct PoolKey { - Currency currency0; - Currency currency1; - uint24 fee; - int24 tickSpacing; - IHooks hooks; - } - - struct ModifyPositionParams { - // the lower and upper tick of the position - int24 tickLower; - int24 tickUpper; - // how to modify the liquidity - int256 liquidityDelta; - } - - function swap(PoolKey memory key, SwapParams memory params) external returns (BalanceDelta); - function donate(PoolKey memory key, uint256 amount0, uint256 amount1) external returns (BalanceDelta); - function modifyPosition(PoolKey memory key, ModifyPositionParams memory params) external returns (BalanceDelta); -} diff --git a/src/contracts/examples/v4-example/UniV4Hook.sol b/src/contracts/examples/v4-example/UniV4Hook.sol deleted file mode 100644 index f6fbc1ddb..000000000 --- a/src/contracts/examples/v4-example/UniV4Hook.sol +++ /dev/null @@ -1,124 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.25; - -// V4 Imports -import { IPoolManager } from "./IPoolManager.sol"; -import { IHooks } from "./IHooks.sol"; - -// Atlas Imports -import { V4DAppControl } from "./V4DAppControl.sol"; - -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; -import { SafetyBits } from "src/contracts/libraries/SafetyBits.sol"; - -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/LockTypes.sol"; - -// NOTE: Uniswap V4 is unique in that it would not require a frontend integration. -// Instead, hooks can be used to enforce that the proceeds of the MEV auctions are -// sent wherever the hook creators wish. In this example, the MEV auction proceeds -// are donated back to the pool. - -///////////////////////////////////////////////////////// -// V4 HOOK // -///////////////////////////////////////////////////////// - -contract UniV4Hook is V4DAppControl { - constructor(address _atlas, address _v4Singleton) V4DAppControl(_atlas, _v4Singleton) { } - - function getHooksCalls() public pure returns (IHooks.Calls memory) { - // override - return IHooks.Calls({ - beforeInitialize: false, - afterInitialize: false, - beforeModifyPosition: true, // <-- - afterModifyPosition: false, - beforeSwap: true, // <-- - afterSwap: false, - beforeDonate: false, - afterDonate: false - }); - } - - function beforeModifyPosition( - address, - PoolKey calldata, - IPoolManager.ModifyPositionParams calldata - ) - external - virtual - returns (bytes4) - { - // TODO: Hook must own ALL liquidity. - // Users can withdraw liquidity through Hook rather than through the pool itself - } - - function beforeSwap( - address sender, - IPoolManager.PoolKey calldata key, - IPoolManager.SwapParams calldata - ) - external - view - returns (bytes4) - { - // This function is a standard call - // address(this) = hook - // msg.sender = v4Singleton - - // Verify that the swapper went through the FastLane Atlas MEV Auction - // and that DAppControl supplied a valid signature - require(address(this) == hook, "ERR-H00 InvalidCallee"); - require(msg.sender == v4Singleton, "ERR-H01 InvalidCaller"); // TODO: Confirm this - - ExecutionPhase currentPhase = ExecutionPhase(_phase()); - - bytes32 hashLock; - assembly { - hashLock := tload(transient_slot_hashLock.slot) - } - - if (currentPhase == ExecutionPhase.UserOperation) { - // Case: User call - // Sender = ExecutionEnvironment - - // Verify that the pool is valid for the user to trade in. - require(keccak256(abi.encode(key, sender)) == hashLock, "ERR-H02 InvalidSwapper"); - } else if (currentPhase == ExecutionPhase.SolverOperation) { - // Case: Solver call - // Sender = Solver contract - // NOTE: This phase verifies that the user's transaction has already - // been executed. - // NOTE: Solvers must have triggered the safetyCallback on the ExecutionEnvironment - // *before* swapping. The safetyCallback sets the ExecutionEnvironment as - - // Verify that the pool is valid for a solver to trade in. - require(hashLock == keccak256(abi.encode(key, _control())), "ERR-H04 InvalidPoolKey"); - } else { - // Case: Other call - // Determine if the sequenced order was processed earlier in the block - bytes32 sequenceKey = keccak256( - abi.encodePacked( - IPoolManager.Currency.unwrap(key.currency0), - IPoolManager.Currency.unwrap(key.currency1), - block.number - ) - ); - - if (!sequenceLock[sequenceKey]) { - // TODO: Add in ability to "cache" the unsequenced transaction in storage. - // Currently, Uni V4 will either fully execute the trade or throw a revert, - // undoing any SSTORE made by the hook. - revert("ERR-H02 InvalidLockStage"); - } - } - - // NOTE: Solvers attempting to backrun in this pool will easily be able to precompute - // the hashLock's value. It should not be used as a lock to keep them out - it is only - // meant to prevent solvers from winning an auction for Pool X but trading in Pool Y. - - return UniV4Hook.beforeSwap.selector; - } -} diff --git a/src/contracts/examples/v4-example/V4DAppControl.sol b/src/contracts/examples/v4-example/V4DAppControl.sol deleted file mode 100644 index 6d0e7684a..000000000 --- a/src/contracts/examples/v4-example/V4DAppControl.sol +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.25; - -// Base Imports -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -// Atlas Base Imports -import { IAtlas } from "src/contracts/interfaces/IAtlas.sol"; -import { SafetyBits } from "src/contracts/libraries/SafetyBits.sol"; - -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/LockTypes.sol"; - -// Atlas DApp-Control Imports -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; - -// V4 Imports -import { IPoolManager } from "./IPoolManager.sol"; -import { IHooks } from "./IHooks.sol"; - -contract V4DAppControl is DAppControl { - struct PreOpsReturn { - address approvedToken; - IPoolManager.PoolKey poolKey; - } - - struct PoolKey { - bool initialized; - IPoolManager.PoolKey key; - } - - bytes4 public constant SWAP = IPoolManager.swap.selector; - address public immutable hook; - address public immutable v4Singleton; - - // Map to track when "Non Adversarial" flow is allowed. - // NOTE: This hook is meant to be used for multiple pairs - // key: keccak(token0, token1, block.number) - mapping(bytes32 => bool) public sequenceLock; - - uint256 transient_slot_initialized = uint256(keccak256("V4DAppControl.initialized")); - uint256 transient_slot_hashLock = uint256(keccak256("V4DAppControl.hashLock")); - - constructor( - address _atlas, - address _v4Singleton - ) - DAppControl( - _atlas, - msg.sender, - CallConfig({ - userNoncesSequential: false, - dappNoncesSequential: false, - requirePreOps: true, - trackPreOpsReturnData: true, - trackUserReturnData: false, - delegateUser: false, - requirePreSolver: false, - requirePostSolver: false, - requirePostOps: true, - zeroSolvers: true, - reuseUserOp: false, - userAuctioneer: true, - solverAuctioneer: true, - unknownAuctioneer: true, - verifyCallChainHash: true, - forwardReturnData: false, - requireFulfillment: true, - trustedOpHash: false, - invertBidValue: false, - exPostBids: false, - allowAllocateValueFailure: false - }) - ) - { - hook = address(this); - v4Singleton = _v4Singleton; - } - - ///////////////////////////////////////////////////////// - // ATLAS CALLS // - ///////////////////////////////////////////////////////// - - function _checkUserOperation(UserOperation memory userOp) internal view override { - require(bytes4(userOp.data) == SWAP, "ERR-H10 InvalidFunction"); - require(userOp.dapp == v4Singleton, "ERR-H11 InvalidTo"); // this is wrong - } - - /////////////// DELEGATED CALLS ////////////////// - function _preOpsCall(UserOperation calldata userOp) internal override returns (bytes memory preOpsData) { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Atlas Escrow - - bool initialized; - assembly { - initialized := tload(transient_slot_initialized.slot) - } - - require(!initialized, "ERR-H09 AlreadyInitialized"); - - (IPoolManager.PoolKey memory key, IPoolManager.SwapParams memory params) = - abi.decode(userOp.data[4:], (IPoolManager.PoolKey, IPoolManager.SwapParams)); - - // Perform more checks and activate the lock - V4DAppControl(hook).setLock(key); - - assembly { - tstore(transient_slot_initialized.slot, 1) - } - - // Handle forwarding of token approvals, or token transfers. - // NOTE: The user will have approved the ExecutionEnvironment in a prior call - PreOpsReturn memory preOpsReturn = PreOpsReturn({ - approvedToken: ( - params.zeroForOne - ? IPoolManager.Currency.unwrap(key.currency0) - : IPoolManager.Currency.unwrap(key.currency1) - ), - poolKey: key - }); - - // TODO: Determine if optimistic transfers are possible - // (An example) - if (params.zeroForOne) { - if (params.amountSpecified > 0) { - // Buying Pool's token1 with amountSpecified of User's token0 - // ERC20(token0).approve(v4Singleton, amountSpecified); - SafeTransferLib.safeTransferFrom( - IPoolManager.Currency.unwrap(key.currency0), - userOp.from, - v4Singleton, // <- TODO: confirm - uint256(params.amountSpecified) - ); - } else { - // Buying amountSpecified of Pool's token1 with User's token0 - } - } else { - if (params.amountSpecified > 0) { - // Buying Pool's token0 with amountSpecified of User's token1 - } - else { - // Buying amountSpecified of Pool's token0 with User's token1 - } - } - - // Return value - preOpsData = abi.encode(preOpsReturn); - } - - // This occurs after a Solver has successfully paid their bid, which is - // held in ExecutionEnvironment. - function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata) internal override { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Escrow - - bool initialized; - assembly { - initialized := tload(transient_slot_initialized.slot) - } - require(!initialized, "ERR-H09 AlreadyInitialized"); - - IPoolManager.PoolKey memory key; // todo: finish - - if (bidToken == IPoolManager.Currency.unwrap(key.currency0)) { - IPoolManager(v4Singleton).donate(key, bidAmount, 0); - } else { - IPoolManager(v4Singleton).donate(key, 0, bidAmount); - } - - // Flag the pool to be open for trading for the remainder of the block - bytes32 sequenceKey = keccak256( - abi.encodePacked( - IPoolManager.Currency.unwrap(key.currency0), IPoolManager.Currency.unwrap(key.currency1), block.number - ) - ); - - sequenceLock[sequenceKey] = true; - } - - function _postOpsCall(bool solved, bytes calldata data) internal override { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Escrow - - if (!solved) revert(); - - (bytes memory returnData) = abi.decode(data, (bytes)); - - PreOpsReturn memory preOpsReturn = abi.decode(returnData, (PreOpsReturn)); - - V4DAppControl(hook).releaseLock(preOpsReturn.poolKey); - - assembly { - tstore(transient_slot_initialized.slot, 0) - } - } - - /////////////// EXTERNAL CALLS ////////////////// - function setLock(IPoolManager.PoolKey memory key) external { - // This function is a standard call - // address(this) = hook - // msg.sender = ExecutionEnvironment - - // Verify that the swapper went through the FastLane Atlas MEV Auction - // and that DAppControl supplied a valid signature - require(address(this) == hook, "ERR-H00 InvalidCallee"); - require(hook == _control(), "ERR-H01 InvalidCaller"); - require(_phase() == uint8(ExecutionPhase.PreOps), "ERR-H02 InvalidLockStage"); - - bytes32 hashLock; - assembly { - hashLock := tload(transient_slot_hashLock.slot) - } - require(hashLock == bytes32(0), "ERR-H03 AlreadyActive"); - - // Set the storage lock to block reentry / concurrent trading - hashLock = keccak256(abi.encode(key, msg.sender)); - assembly { - tstore(transient_slot_hashLock.slot, hashLock) - } - } - - function releaseLock(IPoolManager.PoolKey memory key) external { - // This function is a standard call - // address(this) = hook - // msg.sender = ExecutionEnvironment - - // Verify that the swapper went through the FastLane Atlas MEV Auction - // and that DAppControl supplied a valid signature - require(address(this) == hook, "ERR-H20 InvalidCallee"); - require(hook == _control(), "ERR-H21 InvalidCaller"); - require(_phase() == uint8(ExecutionPhase.PostOps), "ERR-H22 InvalidLockStage"); - - bytes32 hashLock; - assembly { - hashLock := tload(transient_slot_hashLock.slot) - } - require(hashLock == keccak256(abi.encode(key, msg.sender)), "ERR-H23 InvalidKey"); - - // Release the storage lock - assembly { - tstore(transient_slot_hashLock.slot, 0) - } - //delete hashLock; - } - - ///////////////// GETTERS & HELPERS // ////////////////// - - function getBidFormat(UserOperation calldata userOp) public pure override returns (address bidToken) { - // This is a helper function called by solvers - // so that they can get the proper format for - // submitting their bids to the hook. - - (IPoolManager.PoolKey memory key,) = abi.decode(userOp.data, (IPoolManager.PoolKey, IPoolManager.SwapParams)); - - // TODO: need to return whichever token the solvers are trying to buy - return IPoolManager.Currency.unwrap(key.currency0); - } - - function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) { - return solverOp.bidAmount; - } -} diff --git a/test/FLOnline.t.sol b/test/FLOnline.t.sol deleted file mode 100644 index 082507d48..000000000 --- a/test/FLOnline.t.sol +++ /dev/null @@ -1,1925 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import "forge-std/Test.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { BaseTest } from "./base/BaseTest.t.sol"; -import { SolverBase } from "src/contracts/solver/SolverBase.sol"; - -import { SolverOperation } from "src/contracts/types/SolverOperation.sol"; -import { UserOperation } from "src/contracts/types/UserOperation.sol"; - -import { FastLaneOnlineOuter } from "src/contracts/examples/fastlane-online/FastLaneOnlineOuter.sol"; -import { FastLaneOnlineInner } from "src/contracts/examples/fastlane-online/FastLaneOnlineInner.sol"; -import { SwapIntent, BaselineCall, Reputation } from "src/contracts/examples/fastlane-online/FastLaneTypes.sol"; -import { FastLaneOnlineErrors } from "src/contracts/examples/fastlane-online/FastLaneOnlineErrors.sol"; - -import { IUniswapV2Router02 } from "test/base/interfaces/IUniswapV2Router.sol"; - -contract FastLaneOnlineTest is BaseTest { - struct FastOnlineSwapArgs { - UserOperation userOp; - SwapIntent swapIntent; - BaselineCall baselineCall; - uint256 deadline; - uint256 gas; - uint256 maxFeePerGas; - uint256 msgValue; - bytes32 userOpHash; - } - - struct BeforeAndAfterVars { - uint256 userTokenOutBalance; - uint256 userTokenInBalance; - uint256 solverTokenOutBalance; - uint256 solverTokenInBalance; - uint256 atlasGasSurcharge; - Reputation solverOneRep; - Reputation solverTwoRep; - Reputation solverThreeRep; - Reputation solverFourRep; - } - - // defaults to true when solver calls `addSolverOp()`, set to false if the solverOp is expected to not be included - // in the final solverOps array, or if the solverOp is not attempted as it has a higher index in the sorted array - // than the winning solverOp. - struct ExecutionAttemptedInMetacall { - bool solverOne; - bool solverTwo; - bool solverThree; - bool solverFour; - } - - uint256 constant ERR_MARGIN = 0.15e18; // 15% error margin - address internal constant NATIVE_TOKEN = address(0); - - address protocolGuildWallet = 0x25941dC771bB64514Fc8abBce970307Fb9d477e9; - - IUniswapV2Router02 routerV2 = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); - - uint256 goodSolverBidETH = 1.2 ether; // more than baseline swap amountOut if tokenOut is WETH/ETH - uint256 goodSolverBidDAI = 3100e18; // more than baseline swap amountOut if tokenOut is DAI - uint256 defaultMsgValue = 1e16; // 0.01 ETH for bundler gas, treated as donation - uint256 defaultGasLimit = 2_000_000; - uint256 defaultGasPrice; - uint256 defaultDeadlineBlock; - uint256 defaultDeadlineTimestamp; - - // 3200 DAI for 1 WETH (no native tokens) - SwapIntent defaultSwapIntent = SwapIntent({ - tokenUserBuys: address(WETH), - minAmountUserBuys: 1e18, - tokenUserSells: DAI_ADDRESS, - amountUserSells: 3200e18 - }); - - FastLaneOnlineOuter flOnline; - MockFastLaneOnline flOnlineMock; - address executionEnvironment; - - Sig sig; - FastOnlineSwapArgs args; - BeforeAndAfterVars beforeVars; - ExecutionAttemptedInMetacall attempted; - - function setUp() public virtual override { - BaseTest.setUp(); - vm.rollFork(20_385_779); // ETH was just under $3100 at this block - - defaultDeadlineBlock = block.number + 1; - defaultDeadlineTimestamp = block.timestamp + 1; - defaultGasPrice = tx.gasprice; - - vm.startPrank(governanceEOA); - flOnlineMock = new MockFastLaneOnline{ salt: bytes32("1") }(address(atlas), protocolGuildWallet); - flOnline = new FastLaneOnlineOuter(address(atlas), protocolGuildWallet); - atlasVerification.initializeGovernance(address(flOnline)); - // FLOnline contract must be registered as its own signatory - atlasVerification.addSignatory(address(flOnline), address(flOnline)); - // Once set up, burn gov role - only the contract itself should be a signatory - flOnline.transferGovernance(address(govBurner)); - govBurner.burnGovernance(address(flOnline)); - vm.stopPrank(); - - // Get but do not deploy user's EE - first solver registered will deploy it - (executionEnvironment,,) = atlas.getExecutionEnvironment(userEOA, address(flOnline)); - - // NOTE: `_setUpUser()` MUST be called at the start of each end-to-end test. - } - - // ---------------------------------------------------- // - // Scenario Tests // - // ---------------------------------------------------- // - - function testFLOnline_Swap_OneSolverFulfills_Success() public { - _setUpUser(defaultSwapIntent); - - // Set up the solver contract and register the solverOp in the FLOnline contract - address winningSolver = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - - // User calls fastOnlineSwap, do checks that user and solver balances changed as expected - _doFastOnlineSwapWithChecks({ - winningSolverEOA: solverOneEOA, - winningSolver: winningSolver, - solverCount: 1, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_OneSolverFulfills_NativeIn_Success() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: DAI_ADDRESS, - minAmountUserBuys: 3000e18, - tokenUserSells: NATIVE_TOKEN, - amountUserSells: 1e18 - }) - ); - - // Set up the solver contract and register the solverOp in the FLOnline contract - address winningSolver = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidDAI); - - // User calls fastOnlineSwap, do checks that user and solver balances changed as expected - _doFastOnlineSwapWithChecks({ - winningSolverEOA: solverOneEOA, - winningSolver: winningSolver, - solverCount: 1, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_OneSolverFulfills_NativeOut_Success() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: NATIVE_TOKEN, - minAmountUserBuys: 1e18, - tokenUserSells: DAI_ADDRESS, - amountUserSells: 3200e18 - }) - ); - - // Set up the solver contract and register the solverOp in the FLOnline contract - address winningSolver = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - - // User calls fastOnlineSwap, do checks that user and solver balances changed as expected - _doFastOnlineSwapWithChecks({ - winningSolverEOA: solverOneEOA, - winningSolver: winningSolver, - solverCount: 1, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_OneSolverFails_BaselineCallFulfills_Success() public { - _setUpUser(defaultSwapIntent); - - // Set up the solver contract and register the solverOp in the FLOnline contract - address failingSolver = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - - // Check BaselineCall struct is formed correctly and can succeed, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - // Set failingSolver to fail during metacall - FLOnlineRFQSolver(payable(failingSolver)).setShouldSucceed(false); - - // Now fastOnlineSwap should succeed using BaselineCall for fulfillment, with gas + Atlas gas surcharge paid for - // by ETH sent as msg.value by user. - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 1, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_OneSolverFails_BaselineCallFulfills_NativeIn_Success() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: DAI_ADDRESS, - minAmountUserBuys: 3000e18, - tokenUserSells: NATIVE_TOKEN, - amountUserSells: 1e18 - }) - ); - - // Set up the solver contract and register the solverOp in the FLOnline contract - address failingSolver = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidDAI); - - // Check BaselineCall struct is formed correctly and can succeed, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - // Set failingSolver to fail during metacall - FLOnlineRFQSolver(payable(failingSolver)).setShouldSucceed(false); - - // Now fastOnlineSwap should succeed using BaselineCall for fulfillment, with gas + Atlas gas surcharge paid for - // by ETH sent as msg.value by user. - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 1, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_OneSolverFails_BaselineCallFulfills_NativeOut_Success() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: NATIVE_TOKEN, - minAmountUserBuys: 1e18, - tokenUserSells: DAI_ADDRESS, - amountUserSells: 3200e18 - }) - ); - - // Set up the solver contract and register the solverOp in the FLOnline contract - address failingSolver = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - - // Check BaselineCall struct is formed correctly and can succeed, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - // Set failingSolver to fail during metacall - FLOnlineRFQSolver(payable(failingSolver)).setShouldSucceed(false); - - // Now fastOnlineSwap should succeed using BaselineCall for fulfillment, with gas + Atlas gas surcharge paid for - // by ETH sent as msg.value by user. - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 1, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_OneSolverFails_BaselineCallReverts_Failure() public { - _setUpUser(defaultSwapIntent); - - // Set baselineCall incorrectly to intentionally fail - _setBaselineCallToRevert(); - - address solver = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - - // Check BaselineCall struct is formed correctly and can revert, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: false }); - - // Set solver contract to fail during metacall - FLOnlineRFQSolver(payable(solver)).setShouldSucceed(false); - - // fastOnlineSwap should revert if all solvers fail AND the baseline call also fails - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 1, - swapCallShouldSucceed: false // fastOnlineSwap should revert - }); - } - - function testFLOnline_Swap_ZeroSolvers_BaselineCallFulfills_Success() public { - _setUpUser(defaultSwapIntent); - - // No solverOps at all - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - // Now fastOnlineSwap should succeed using BaselineCall for fulfillment, with gas + Atlas gas surcharge paid for - // by ETH sent as msg.value by user. - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 0, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_ZeroSolvers_BaselineCallFulfills_NativeIn_Success() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: DAI_ADDRESS, - minAmountUserBuys: 3000e18, - tokenUserSells: NATIVE_TOKEN, - amountUserSells: 1e18 - }) - ); - - // Check BaselineCall struct is formed correctly and can succeed, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - // Now fastOnlineSwap should succeed using BaselineCall for fulfillment, with gas + Atlas gas surcharge paid for - // by ETH sent as msg.value by user. - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 0, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_ZeroSolvers_BaselineCallFulfills_NativeOut_Success() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: NATIVE_TOKEN, - minAmountUserBuys: 1e18, - tokenUserSells: DAI_ADDRESS, - amountUserSells: 3200e18 - }) - ); - - // Check BaselineCall struct is formed correctly and can succeed, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - // Now fastOnlineSwap should succeed using BaselineCall for fulfillment, with gas + Atlas gas surcharge paid for - // by ETH sent as msg.value by user. - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 0, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_ZeroSolvers_BaselineCallReverts_Failure() public { - _setUpUser(defaultSwapIntent); - - // Set baselineCall incorrectly to intentionally fail - _setBaselineCallToRevert(); - - // Check BaselineCall struct is formed correctly and can revert, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: false }); - - // fastOnlineSwap should revert if all solvers fail AND the baseline call also fails - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 0, - swapCallShouldSucceed: false // fastOnlineSwap should revert - }); - } - - function testFLOnline_Swap_ZeroSolvers_BaselineCallReverts_NativeIn_Failure() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: DAI_ADDRESS, - minAmountUserBuys: 3000e18, - tokenUserSells: NATIVE_TOKEN, - amountUserSells: 1e18 - }) - ); - - // Set baselineCall incorrectly to intentionally fail - _setBaselineCallToRevert(); - - // Check BaselineCall struct is formed correctly and can revert, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: false }); - - // fastOnlineSwap should revert if all solvers fail AND the baseline call also fails - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 0, - swapCallShouldSucceed: false // fastOnlineSwap should revert - }); - } - - function testFLOnline_Swap_ZeroSolvers_BaselineCallReverts_NativeOut_Failure() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: NATIVE_TOKEN, - minAmountUserBuys: 1e18, - tokenUserSells: DAI_ADDRESS, - amountUserSells: 3200e18 - }) - ); - - // Set baselineCall incorrectly to intentionally fail - _setBaselineCallToRevert(); - - // Check BaselineCall struct is formed correctly and can revert, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: false }); - - // fastOnlineSwap should revert if all solvers fail AND the baseline call also fails - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 0, - swapCallShouldSucceed: false // fastOnlineSwap should revert - }); - } - - function testFLOnline_Swap_ThreeSolvers_ThirdFulfills_Success() public { - _setUpUser(defaultSwapIntent); - - // Set up the solver contracts and register the solverOps in the FLOnline contract - _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - _setUpSolver(solverTwoEOA, solverTwoPK, goodSolverBidETH + 1e17); - address winningSolver = _setUpSolver(solverThreeEOA, solverThreePK, goodSolverBidETH + 2e17); - - // solverOne does not get included in the sovlerOps array - attempted.solverOne = false; - // solverTwo has a lower bid than winner (solverThree) so is not attempted - attempted.solverTwo = false; - - // User calls fastOnlineSwap, do checks that user and solver balances changed as expected - _doFastOnlineSwapWithChecks({ - winningSolverEOA: solverThreeEOA, - winningSolver: winningSolver, - solverCount: 3, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_ThreeSolvers_ThirdFulfills_NativeIn_Success() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: DAI_ADDRESS, - minAmountUserBuys: 3000e18, - tokenUserSells: NATIVE_TOKEN, - amountUserSells: 1e18 - }) - ); - - // Set up the solver contracts and register the solverOps in the FLOnline contract - _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidDAI); - _setUpSolver(solverTwoEOA, solverTwoPK, goodSolverBidDAI + 1e17); - address winningSolver = _setUpSolver(solverThreeEOA, solverThreePK, goodSolverBidDAI + 2e17); - - // solverOne does not get included in the sovlerOps array - attempted.solverOne = false; - // solverTwo has a lower bid than winner (solverThree) so is not attempted - attempted.solverTwo = false; - - // User calls fastOnlineSwap, do checks that user and solver balances changed as expected - _doFastOnlineSwapWithChecks({ - winningSolverEOA: solverThreeEOA, - winningSolver: winningSolver, - solverCount: 3, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_ThreeSolvers_ThirdFulfills_NativeOut_Success() public { - _setUpUser( - SwapIntent({ - tokenUserBuys: NATIVE_TOKEN, - minAmountUserBuys: 1e18, - tokenUserSells: DAI_ADDRESS, - amountUserSells: 3200e18 - }) - ); - - // Set up the solver contracts and register the solverOps in the FLOnline contract - _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - _setUpSolver(solverTwoEOA, solverTwoPK, goodSolverBidETH + 1e17); - address winningSolver = _setUpSolver(solverThreeEOA, solverThreePK, goodSolverBidETH + 2e17); - - // solverOne does not get included in the sovlerOps array - attempted.solverOne = false; - // solverTwo has a lower bid than winner (solverThree) so is not attempted - attempted.solverTwo = false; - - // User calls fastOnlineSwap, do checks that user and solver balances changed as expected - _doFastOnlineSwapWithChecks({ - winningSolverEOA: solverThreeEOA, - winningSolver: winningSolver, - solverCount: 3, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_ThreeSolvers_AllFail_BaselineCallFulfills_Success() public { - _setUpUser(defaultSwapIntent); - - // Set up the solver contracts and register the solverOps in the FLOnline contract - address solver1 = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - address solver2 = _setUpSolver(solverTwoEOA, solverTwoPK, goodSolverBidETH + 1e17); - address solver3 = _setUpSolver(solverThreeEOA, solverThreePK, goodSolverBidETH + 2e17); - // all 3 solvers will be included and attempted but fail - - // Check BaselineCall struct is formed correctly and can succeed, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - // Set all solvers to fail during metacall - FLOnlineRFQSolver(payable(solver1)).setShouldSucceed(false); - FLOnlineRFQSolver(payable(solver2)).setShouldSucceed(false); - FLOnlineRFQSolver(payable(solver3)).setShouldSucceed(false); - - // Now fastOnlineSwap should succeed using BaselineCall for fulfillment, with gas + Atlas gas surcharge paid for - // by ETH sent as msg.value by user. - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 3, - swapCallShouldSucceed: true - }); - } - - function testFLOnline_Swap_ThreeSolvers_AllFail_BaselineCallReverts_Failure() public { - _setUpUser(defaultSwapIntent); - - // Set up the solver contracts and register the solverOps in the FLOnline contract - address solver1 = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - address solver2 = _setUpSolver(solverTwoEOA, solverTwoPK, goodSolverBidETH + 1e17); - address solver3 = _setUpSolver(solverThreeEOA, solverThreePK, goodSolverBidETH + 2e17); - - // solverOne does not get included in the sovlerOps array - attempted.solverOne = false; - // solverTwo and solverThree will be attempted but fail - - // Set baselineCall incorrectly to intentionally fail - _setBaselineCallToRevert(); - - // Check BaselineCall struct is formed correctly and can revert, revert changes after - _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: false }); - - // Set all solvers to fail during metacall - FLOnlineRFQSolver(payable(solver1)).setShouldSucceed(false); - FLOnlineRFQSolver(payable(solver2)).setShouldSucceed(false); - FLOnlineRFQSolver(payable(solver3)).setShouldSucceed(false); - - // fastOnlineSwap should revert if all solvers fail AND the baseline call also fails - _doFastOnlineSwapWithChecks({ - winningSolverEOA: address(0), - winningSolver: address(0), // No winning solver expected - solverCount: 3, - swapCallShouldSucceed: false // fastOnlineSwap should revert - }); - } - - function testFLOnline_Swap_SolverBidsSameAsBaselineCall_Success() public { - _setUpUser(defaultSwapIntent); - - uint256 baselineAmountOut = _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - // If solver bids below baseline amountOut, addSolverOp will revert - bytes4 expectedErr = FastLaneOnlineErrors.SolverGateway_AddSolverOp_SimulationFail.selector; - _setUpSolver(solverOneEOA, solverOnePK, baselineAmountOut - 1, expectedErr); - - // But if solver bids equal to baseline amountOut, solver will win if no higher bids - address winningSolver = _setUpSolver(solverOneEOA, solverOnePK, baselineAmountOut); - - // Now fastOnlineSwap should succeed using BaselineCall for fulfillment, with gas + Atlas gas surcharge paid for - // by ETH sent as msg.value by user. - _doFastOnlineSwapWithChecks({ - winningSolverEOA: solverOneEOA, - winningSolver: winningSolver, - solverCount: 1, - swapCallShouldSucceed: true - }); - } - - // ---------------------------------------------------- // - // addSolverOp() Tests // - // ---------------------------------------------------- // - - function testFLOnline_addSolverOp_FirstSolverCreatesEE() public { - bool isDeployed; - _setUpUser(defaultSwapIntent); - - (,, isDeployed) = atlas.getExecutionEnvironment(userEOA, address(flOnline)); - assertEq(isDeployed, false, "EE should not be deployed yet"); - - // First solver to call addSolverOp() deploys EE - _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - - (,, isDeployed) = atlas.getExecutionEnvironment(userEOA, address(flOnline)); - assertEq(isDeployed, true, "EE should be deployed"); - } - - function testFLOnline_addSolverOp_WrongCaller_Fails() public { - _setUpUser(defaultSwapIntent); - SolverOperation memory solverOp = _buildSolverOp(solverOneEOA, solverOnePK, address(123), 1); - - vm.prank(userEOA); // Should revert if caller not solverOneEOA - vm.expectRevert(FastLaneOnlineErrors.SolverGateway_AddSolverOp_SolverMustBeSender.selector); - flOnline.addSolverOp(args.userOp, solverOp); - } - - function testFLOnline_addSolverOp_SimFail_Fails() public { - _setUpUser(defaultSwapIntent); - - vm.startPrank(solverOneEOA); // standard solver set up - FLOnlineRFQSolver solver = new FLOnlineRFQSolver{ salt: keccak256("salt") }(WETH_ADDRESS, address(atlas)); - SolverOperation memory solverOp = _buildSolverOp(solverOneEOA, solverOnePK, address(solver), goodSolverBidETH); - - // Set solver contract to revert, causing sim to fail - solver.setShouldSucceed(false); - - vm.expectRevert(FastLaneOnlineErrors.SolverGateway_AddSolverOp_SimulationFail.selector); - flOnline.addSolverOp(args.userOp, solverOp); - } - - function testFLOnline_addSolverOp_ThreeNew() public { - _setUpUser(defaultSwapIntent); - uint256 buyInAmount = 1e17; - deal(solverOneEOA, buyInAmount); - deal(solverTwoEOA, buyInAmount); - deal(solverThreeEOA, buyInAmount); - - bytes32[] memory solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 0, "solverOpHashes should start empty"); - - SolverOperation memory solverOp1 = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH, buyInAmount); - - solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 1, "solverOpHashes should have 1 element"); - assertEq(solverOpHashes[0], keccak256(abi.encode(solverOp1)), "solverOpHashes[0] should be keccak(solverOp1)"); - assertEq( - flOnline.congestionBuyIn(keccak256(abi.encode(solverOp1))), - buyInAmount, - "solverOp1 buy in should be buyInAmount" - ); - assertEq( - flOnline.aggCongestionBuyIn(args.userOpHash), buyInAmount, "aggCongestionBuyIn should be 1x buyInAmount" - ); - - SolverOperation memory solverOp2 = _setUpSolver(solverTwoEOA, solverTwoPK, goodSolverBidETH, buyInAmount); - - solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 2, "solverOpHashes should have 2 elements"); - assertEq(solverOpHashes[1], keccak256(abi.encode(solverOp2)), "solverOpHashes[1] should be keccak(solverOp2)"); - assertEq( - flOnline.congestionBuyIn(keccak256(abi.encode(solverOp2))), - buyInAmount, - "solverOp2 buy in should be buyInAmount" - ); - assertEq( - flOnline.aggCongestionBuyIn(args.userOpHash), 2 * buyInAmount, "aggCongestionBuyIn should be 2x buyInAmount" - ); - - SolverOperation memory solverOp3 = _setUpSolver(solverThreeEOA, solverThreePK, goodSolverBidETH, buyInAmount); - - solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 3, "solverOpHashes should have 3 elements"); - assertEq(solverOpHashes[2], keccak256(abi.encode(solverOp3)), "solverOpHashes[2] should be keccak(solverOp3)"); - assertEq( - flOnline.congestionBuyIn(keccak256(abi.encode(solverOp3))), - buyInAmount, - "solverOp3 buy in should be buyInAmount" - ); - assertEq( - flOnline.aggCongestionBuyIn(args.userOpHash), 3 * buyInAmount, "aggCongestionBuyIn should be 3x buyInAmount" - ); - } - - function testFLOnline_addSolverOp_ThreeNew_FourthBidsHigher_ReplacesFirst() public { - // Similar to test above, but here the 4th solver will not fit into the userOp.gas limit imposed on the - // metacall. When it has a higher congestion buy-in than the others it should replace solverOp1. - _setUpUser(defaultSwapIntent); - uint256 buyInAmount = 1e17; - deal(solverOneEOA, buyInAmount); - deal(solverTwoEOA, buyInAmount); - deal(solverThreeEOA, buyInAmount); - deal(solverFourEOA, buyInAmount * 2); // double normal buy-in - - bytes32[] memory solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 0, "solverOpHashes should start empty"); - - SolverOperation memory solverOp1 = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH, buyInAmount); - SolverOperation memory solverOp2 = _setUpSolver(solverTwoEOA, solverTwoPK, goodSolverBidETH, buyInAmount); - SolverOperation memory solverOp3 = _setUpSolver(solverThreeEOA, solverThreePK, goodSolverBidETH, buyInAmount); - - solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 3, "solverOpHashes should have 3 elements"); - assertEq(solverOpHashes[0], keccak256(abi.encode(solverOp1)), "solverOpHashes[0] should be keccak(solverOp1)"); - assertEq(solverOpHashes[1], keccak256(abi.encode(solverOp2)), "solverOpHashes[1] should be keccak(solverOp2)"); - assertEq(solverOpHashes[2], keccak256(abi.encode(solverOp3)), "solverOpHashes[2] should be keccak(solverOp3)"); - assertEq(flOnline.aggCongestionBuyIn(args.userOpHash), 3 * buyInAmount, "total buy in expected buyInAmountx3"); - - // Now add 4th solverOp with higher congestion buy-in - should replace 1st solverOp - SolverOperation memory solverOp4 = _setUpSolver(solverFourEOA, solverFourPK, goodSolverBidETH, buyInAmount * 2); - - solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 3, "solverOpHashes should still have 3 elements"); - assertEq(solverOpHashes[0], keccak256(abi.encode(solverOp4)), "solverOpHashes[0] should be keccak(solverOp4)"); - assertEq(solverOpHashes[1], keccak256(abi.encode(solverOp2)), "solverOpHashes[1] should be keccak(solverOp2)"); - assertEq(solverOpHashes[2], keccak256(abi.encode(solverOp3)), "solverOpHashes[2] should be keccak(solverOp3)"); - assertEq(flOnline.aggCongestionBuyIn(args.userOpHash), 4 * buyInAmount, "total buy in expected buyInAmountx4"); - } - - function testFLOnline_addSolverOp_ThreeNew_FourthBidsLower_Fails() public { - // Similar to test above, but here the 4th solver sends a lower congestion buy-in, does not get included at all, - // and addSolverOp reverts. - _setUpUser(defaultSwapIntent); - uint256 buyInAmount = 1e17; - deal(solverOneEOA, buyInAmount); - deal(solverTwoEOA, buyInAmount); - deal(solverThreeEOA, buyInAmount); - deal(solverFourEOA, buyInAmount / 2); // half normal buy-in - - bytes32[] memory solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 0, "solverOpHashes should start empty"); - - SolverOperation memory solverOp1 = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH, buyInAmount); - SolverOperation memory solverOp2 = _setUpSolver(solverTwoEOA, solverTwoPK, goodSolverBidETH, buyInAmount); - SolverOperation memory solverOp3 = _setUpSolver(solverThreeEOA, solverThreePK, goodSolverBidETH, buyInAmount); - - solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 3, "solverOpHashes should have 3 elements"); - assertEq(solverOpHashes[0], keccak256(abi.encode(solverOp1)), "solverOpHashes[0] should be keccak(solverOp1)"); - assertEq(solverOpHashes[1], keccak256(abi.encode(solverOp2)), "solverOpHashes[1] should be keccak(solverOp2)"); - assertEq(solverOpHashes[2], keccak256(abi.encode(solverOp3)), "solverOpHashes[2] should be keccak(solverOp3)"); - assertEq(flOnline.aggCongestionBuyIn(args.userOpHash), 3 * buyInAmount, "total buy in expected buyInAmountx3"); - - // Now add 4th solverOp with lower congestion buy-in - should fail - bytes4 expectedErr = FastLaneOnlineErrors.SolverGateway_AddSolverOp_ScoreTooLow.selector; - _setUpSolver(solverFourEOA, solverFourPK, goodSolverBidETH, buyInAmount / 2, expectedErr); - - // Registered solverOp state same as before 4th solver attempt - solverOpHashes = flOnline.solverOpHashes(args.userOpHash); - assertEq(solverOpHashes.length, 3, "solverOpHashes should have 3 elements"); - assertEq(solverOpHashes[0], keccak256(abi.encode(solverOp1)), "solverOpHashes[0] should be keccak(solverOp1)"); - assertEq(solverOpHashes[1], keccak256(abi.encode(solverOp2)), "solverOpHashes[1] should be keccak(solverOp2)"); - assertEq(solverOpHashes[2], keccak256(abi.encode(solverOp3)), "solverOpHashes[2] should be keccak(solverOp3)"); - assertEq(flOnline.aggCongestionBuyIn(args.userOpHash), 3 * buyInAmount, "total buy in expected buyInAmountx3"); - } - - // ---------------------------------------------------- // - // Reputation Tests // - // ---------------------------------------------------- // - - function testFLOnline_CalculateBidFactor() public { - // bidFactor measured with scale = 100. I.e. 100% = 100 - uint256 BASE_FACTOR = flOnlineMock.SLIPPAGE_BASE(); - uint256 MAX_FACTOR = flOnlineMock.GLOBAL_MAX_SLIPPAGE(); - uint256 bidFactor; - - // Case: solver bids minAmountUserBuys + 1 ==> bidFactor should be 100 (floor) - bidFactor = flOnlineMock.calculateBidFactor({ bidAmount: 1e18 + 1, minAmountUserBuys: 1e18 }); - assertEq(bidFactor, BASE_FACTOR, "bidFactor should be floor (+1 bid)"); - - // Case: solver bids under minAmountUserBuys ==> bidFactor should be 100 (floor) - bidFactor = flOnlineMock.calculateBidFactor({ bidAmount: 1e18 - 1, minAmountUserBuys: 1e18 }); - assertEq(bidFactor, BASE_FACTOR, "bidFactor should be floor (underbid)"); - - // Case: solver bids minAmountUserBuys ==> bidFactor should be 100 (floor) - bidFactor = flOnlineMock.calculateBidFactor({ bidAmount: 1e18, minAmountUserBuys: 1e18 }); - assertEq(bidFactor, BASE_FACTOR, "bidFactor should be floor (match)"); - - // Case: still returns floor if minAmountUserBuys is sqrt(type(uint256).max) - bidFactor = flOnlineMock.calculateBidFactor({ bidAmount: 1e18, minAmountUserBuys: sqrt(type(uint256).max) }); - assertEq(bidFactor, BASE_FACTOR, "bidFactor should be floor (max minAmountUserBuys)"); - - // Case: solver bids 2x minAmountUserBuys ==> bidFactor should be 125 (cap) - bidFactor = flOnlineMock.calculateBidFactor({ bidAmount: 2e18, minAmountUserBuys: 1e18 }); - assertEq(bidFactor, MAX_FACTOR, "bidFactor should be max (cap)"); - - // Case: still returns max if bidAmount is sqrt(type(uint256).max) / 100 - bidFactor = - flOnlineMock.calculateBidFactor({ bidAmount: sqrt(type(uint256).max) / 100, minAmountUserBuys: 1e18 }); - assertEq(bidFactor, MAX_FACTOR, "bidFactor should be max (max bidAmount)"); - - // Case: solver bids 1.1x minAmountUserBuys ==> bidFactor should be 120 - bidFactor = flOnlineMock.calculateBidFactor({ bidAmount: 1.1e18, minAmountUserBuys: 1e18 }); - assertEq(bidFactor, 120, "bidFactor should be 120"); - - // Case: reverts if bidAmount > sqrt(type(uint256).max / 100) - // NOTE: This is caught in addSolverOp before revert can happen - vm.expectRevert(); - flOnlineMock.calculateBidFactor({ bidAmount: sqrt(type(uint256).max / 100) + 1, minAmountUserBuys: 1e18 }); - } - - function testFLOnline_CalculateWeightedScore() public { - Reputation memory rep; - uint256 highScore; - uint256 lowScore; - - rep = Reputation({ - successCost: 1e16, // 0.01 ETH on winning solverOps - failureCost: 0 // no failing solverOps - }); - - // Case: totalGas (userOp.gas) positively impacts score - highScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - lowScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit / 2, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - assertTrue(highScore > lowScore, "totalGas should positively impact score"); - - // Case: solverOpGas negatively impacts score - highScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: 200_000, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - lowScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: 300_000, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - assertTrue(highScore > lowScore, "solverOpGas should negatively impact score"); - - // Case: maxFeePerGas positively impacts score - highScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: 20e9, // 20 gwei - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - lowScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: 5e9, // 5 gwei - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - assertTrue(highScore > lowScore, "maxFeePerGas should positively impact score"); - - // Case: congestionBuyIn positively impacts score - highScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 1e17, // 0.1 ETH - solverCount: 1, - bidFactor: 100, - rep: rep - }); - lowScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - assertTrue(highScore > lowScore, "congestionBuyIn should positively impact score"); - - // Case: solverCount negatively impacts score - highScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - lowScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 2, - bidFactor: 100, - rep: rep - }); - assertTrue(highScore > lowScore, "solverCount should negatively impact score"); - - // Case: bidFactor positively impacts score - highScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 120, - rep: rep - }); - lowScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: rep - }); - assertTrue(highScore > lowScore, "bidFactor should positively impact score"); - - // Case: rep.successCost positively impacts score - highScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: Reputation({ - successCost: 1e17, // 0.1 ETH on winning solverOps - failureCost: 0 // no failing solverOps - }) - }); - lowScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: Reputation({ - successCost: 0, // 0 ETH on winning solverOps - failureCost: 0 // no failing solverOps - }) - }); - assertTrue(highScore > lowScore, "rep.successCost should positively impact score"); - - // Case: rep.failureCost negatively impacts score - highScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: Reputation({ - successCost: 0, // 0 ETH on winning solverOps - failureCost: 0 // no failing solverOps - }) - }); - lowScore = flOnlineMock.calculateWeightedScore({ - totalGas: defaultGasLimit, - solverOpGas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - congestionBuyIn: 0, - solverCount: 1, - bidFactor: 100, - rep: Reputation({ - successCost: 0, // 0 ETH on winning solverOps - failureCost: 1e17 // 0.1 ETH on failing solverOps - }) - }); - assertTrue(highScore > lowScore, "rep.failureCost should negatively impact score"); - } - - // ---------------------------------------------------- // - // Congestion Buy-In Tests // - // ---------------------------------------------------- // - - function testFLOnline_RefundCongestionBuyIns() public { - uint256 congestionBuyIn = 1e17; - _setUpUser(defaultSwapIntent); - - // Solver registers with 1e17 congestion buy-in - SolverOperation memory solverOp = _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH, congestionBuyIn); - uint256 solverBalanceBefore = address(solverOneEOA).balance; - - assertTrue(block.number < solverOp.deadline, "should not be past solverOp.deadline"); - - vm.prank(solverOneEOA); - vm.expectRevert(FastLaneOnlineErrors.SolverGateway_RefundCongestionBuyIns_DeadlineNotPassed.selector); - flOnline.refundCongestionBuyIns(solverOp); - - uint256 snapshotId = vm.snapshot(); - - _doFastOnlineSwapWithChecks({ - winningSolverEOA: solverOneEOA, - winningSolver: solverOp.solver, - solverCount: 1, - swapCallShouldSucceed: true - }); - - // Skip forward past solverOp.deadline - vm.roll(solverOp.deadline + 1); - assertTrue(block.number > solverOp.deadline, "now should be past solverOp.deadline"); - - vm.prank(solverOneEOA); - flOnline.refundCongestionBuyIns(solverOp); - - assertEq(address(solverOneEOA).balance, solverBalanceBefore, "solver should not get refund if executed"); - - vm.revertTo(snapshotId); // go back to before the swap - - // Skip forward past solverOp.deadline - vm.roll(solverOp.deadline + 1); - assertTrue(block.number > solverOp.deadline, "now should be past solverOp.deadline"); - - vm.prank(solverOneEOA); - flOnline.refundCongestionBuyIns(solverOp); - - assertEq(address(solverOneEOA).balance, solverBalanceBefore + congestionBuyIn, "cbi not refunded"); - } - - function testFLOnline_ProcessCongestionRake() public { - bytes32 userOpHash = keccak256("userOpHash"); - uint256 aggCongestionBuyIn = 5e17; // 0.5 ETH - uint256 atlasBundlerRebate = 1e17; // 0.1 ETH - uint256 snapshotId; - uint256 netGasRefund; - uint256 expectedRake; - uint256 protocolGuildBefore = protocolGuildWallet.balance; - - // Scenario setup for mock testing: - // - Solver gave a congestion buy-in of 0.5 ETH, which is also the FLO starting balance - // - FLOnline received a 0.1 ETH gas rebate after the metacall from Atlas - - flOnlineMock.setAggCongestionBuyIn(userOpHash, aggCongestionBuyIn); - deal(address(flOnlineMock), aggCongestionBuyIn + atlasBundlerRebate); - assertEq(flOnlineMock.rake(), 0, "rake should start at 0"); - assertEq(flOnlineMock.aggCongestionBuyIn(userOpHash), aggCongestionBuyIn, "aggCongestionBuyIn should be set"); - - snapshotId = vm.snapshot(); // checkpoint to start tests from - - // Case: solverSuccessful = true - // -> rake increases by: (atlasGasRebate + aggCongestionBuyIn) * rake cut - // -> netGasRefund should be: atlasGasRebate + aggCongestionBuyIn - rake from both - // -> aggCongestionBuyIn[userOpHash] set to 0 - // -> no funds sent to protocol guild - - netGasRefund = flOnlineMock.processCongestionRake({ - startingBalance: aggCongestionBuyIn, - userOpHash: userOpHash, - solversSuccessful: true - }); - - expectedRake = (aggCongestionBuyIn + atlasBundlerRebate) * 33 / 100; // 33% rake cut - assertEq(flOnlineMock.rake(), expectedRake, "rake should increase correctly"); - assertEq( - netGasRefund, - aggCongestionBuyIn + atlasBundlerRebate - expectedRake, - "netGasRefund expected: gas rebate + aggCongestionBuyIn - rake" - ); - assertEq(flOnlineMock.aggCongestionBuyIn(userOpHash), 0, "aggCongestionBuyIn should be set to 0"); - assertEq(protocolGuildWallet.balance, protocolGuildBefore, "no funds should be sent to protocol guild"); - - vm.revertTo(snapshotId); // restart from checkpoint - - // Case: solverSuccessful = false - // -> rake increases by: (atlasGasRebate only) * rake cut - // -> netGasRefund should be: atlasGasRebate - rake - // -> aggCongestionBuyIn[userOpHash] set to 0 - // -> protocol guild gets congestionBuyIn funds - - netGasRefund = flOnlineMock.processCongestionRake({ - startingBalance: aggCongestionBuyIn, - userOpHash: userOpHash, - solversSuccessful: false - }); - - expectedRake = atlasBundlerRebate * 33 / 100; // 33% rake cut - assertEq(flOnlineMock.rake(), expectedRake, "rake should increase correctly"); - assertEq(netGasRefund, atlasBundlerRebate - expectedRake, "netGasRefund expected: gas rebate - rake"); - assertEq(flOnlineMock.aggCongestionBuyIn(userOpHash), 0, "aggCongestionBuyIn should be set to 0"); - assertEq( - protocolGuildWallet.balance, protocolGuildBefore + aggCongestionBuyIn, "protocol guild should get funds" - ); - } - - // ---------------------------------------------------- // - // Other Unit Tests // - // ---------------------------------------------------- // - - function testFLOnline_SortSolverOps_SortsInDescendingOrderOfBid() public { - SolverOperation[] memory solverOps = new SolverOperation[](0); - SolverOperation[] memory solverOpsOut; - - // Empty array should return empty array - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - assertEq(solverOpsOut.length, 0, "Not length 0"); - assertTrue(_isSolverOpsSorted(solverOpsOut), "Empty array, not sorted"); - - // 1 solverOp array should return same array - solverOps = new SolverOperation[](1); - solverOps[0].bidAmount = 1; - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - assertEq(solverOpsOut[0].bidAmount, solverOps[0].bidAmount, "1 solverOp array, not same array"); - assertEq(solverOpsOut.length, 1, "Not length 1"); - assertTrue(_isSolverOpsSorted(solverOpsOut), "1 solverOp array, not sorted"); - - // 2 solverOps array should return same array if already sorted - solverOps = new SolverOperation[](2); - solverOps[0].bidAmount = 2; - solverOps[1].bidAmount = 1; - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - assertEq(solverOpsOut[0].bidAmount, solverOps[0].bidAmount, "2 solverOps array, [0] bid mismatch"); - assertEq(solverOpsOut[1].bidAmount, solverOps[1].bidAmount, "2 solverOps array, [1] bid mismatch"); - assertTrue(_isSolverOpsSorted(solverOpsOut), "2 solverOps array, not sorted"); - - // 2 solverOps array should return sorted array if not sorted - solverOps[0].bidAmount = 1; - solverOps[1].bidAmount = 2; - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - assertEq(solverOpsOut[0].bidAmount, solverOps[1].bidAmount, "2 solverOps array, [1] should be in [0]"); - assertEq(solverOpsOut[1].bidAmount, solverOps[0].bidAmount, "2 solverOps array, [0] should be in [1]"); - assertTrue(_isSolverOpsSorted(solverOpsOut), "2 solverOps array, not sorted"); - - // 5 solverOps already sorted (descending) should return same array - solverOps = new SolverOperation[](5); - for (uint256 i = 0; i < 5; i++) { - solverOps[i].bidAmount = 5 - i; - } - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - for (uint256 i = 0; i < 5; i++) { - assertEq(solverOpsOut[i].bidAmount, solverOps[i].bidAmount, "5 solverOps sorted, bid mismatch"); - } - assertEq(solverOpsOut.length, 5, "Not length 5, sorted"); - assertTrue(_isSolverOpsSorted(solverOpsOut), "5 solverOps array, not sorted"); - - // 5 solverOps in ascending order should return sorted (descending) array - for (uint256 i = 0; i < 5; i++) { - solverOps[i].bidAmount = i + 1; - } - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - for (uint256 i = 0; i < 5; i++) { - assertEq(solverOpsOut[i].bidAmount, 5 - i, "5 solverOps opposite order, bid mismatch"); - } - assertEq(solverOpsOut.length, 5, "Not length 5, opposite order"); - assertTrue(_isSolverOpsSorted(solverOpsOut), "5 solverOps opposite order, not sorted"); - - // 5 solverOps in random order should return sorted (descending) array - solverOps[0].bidAmount = 3; - solverOps[1].bidAmount = 1; - solverOps[2].bidAmount = 5; - solverOps[3].bidAmount = 2; - solverOps[4].bidAmount = 4; - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - assertEq(solverOpsOut.length, 5, "Not length 5, random order"); - assertTrue(_isSolverOpsSorted(solverOpsOut), "5 solverOps random order, not sorted"); - } - - function testFLOnline_SortSolverOps_DropsZeroBids() public { - SolverOperation[] memory solverOps = new SolverOperation[](5); - SolverOperation[] memory solverOpsOut; - - // 5 solverOps with 0 bid should return empty array - for (uint256 i = 0; i < 5; i++) { - solverOps[i].bidAmount = 0; - } - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - assertEq(solverOpsOut.length, 0, "5 solverOps with 0 bid, not empty"); - - // 5 solverOps with 0 bid mixed with non-zero bids should return sorted array with 0 bids dropped - solverOps[0].bidAmount = 0; - solverOps[1].bidAmount = 1; - solverOps[2].bidAmount = 0; - solverOps[3].bidAmount = 2; - solverOps[4].bidAmount = 0; - solverOpsOut = flOnlineMock.sortSolverOps(solverOps); - - assertEq(solverOpsOut.length, 2, "5 solverOps with 0 bid mixed, not length 2"); - assertEq(solverOpsOut[0].bidAmount, 2, "5 solverOps with 0 bid mixed, [0] bid mismatch"); - assertEq(solverOpsOut[1].bidAmount, 1, "5 solverOps with 0 bid mixed, [1] bid mismatch"); - } - - function testFLOnline_SetWinningSolver_DoesNotUpdateForUnexpectedCaller() public { - (address userEE,,) = atlas.getExecutionEnvironment(userEOA, address(flOnlineMock)); - assertEq(flOnlineMock.getWinningSolver(), address(0), "winningSolver should start empty"); - - vm.prank(userEOA); - flOnlineMock.setWinningSolver(solverOneEOA); - assertEq(flOnlineMock.getWinningSolver(), address(0), "err 1 - winningSolver should be empty"); - - vm.prank(governanceEOA); - flOnlineMock.setWinningSolver(solverOneEOA); - assertEq(flOnlineMock.getWinningSolver(), address(0), "err 2 - winningSolver should be empty"); - - vm.prank(solverOneEOA); - flOnlineMock.setWinningSolver(solverOneEOA); - assertEq(flOnlineMock.getWinningSolver(), address(0), "err 3 - winningSolver should be empty"); - - vm.prank(address(flOnlineMock)); - flOnlineMock.setWinningSolver(solverOneEOA); - assertEq(flOnlineMock.getWinningSolver(), address(0), "err 4 - winningSolver should be empty"); - - vm.prank(address(atlas)); - flOnlineMock.setWinningSolver(solverOneEOA); - assertEq(flOnlineMock.getWinningSolver(), address(0), "err 5 - winningSolver should be empty"); - - vm.prank(userEE); // userEE is valid caller, but still wont set if userEOA is not in userLock - flOnlineMock.setWinningSolver(solverOneEOA); - assertEq(flOnlineMock.getWinningSolver(), address(0), "err 6 - winningSolver should be empty"); - - // Only valid caller: the user's EE when userEOA is stored in userLock - flOnlineMock.setUserLock(userEOA); - assertEq(flOnlineMock.getUserLock(), userEOA, "userLock should be userEOA"); - vm.prank(userEE); - flOnlineMock.setWinningSolver(solverOneEOA); - assertEq(flOnlineMock.getWinningSolver(), solverOneEOA, "winningSolver should be solverOneEOA"); - } - - function testFLOnline_SetWinningSolver_DoesNotUpdateIfAlreadySet() public { - (address userEE,,) = atlas.getExecutionEnvironment(userEOA, address(flOnlineMock)); - assertEq(flOnlineMock.getWinningSolver(), address(0), "winningSolver should start empty"); - - flOnlineMock.setUserLock(userEOA); - vm.prank(userEE); - flOnlineMock.setWinningSolver(solverOneEOA); - assertEq(flOnlineMock.getWinningSolver(), solverOneEOA, "winningSolver should be solverOneEOA"); - - // winningSolver already set, should not update - vm.prank(userEE); - flOnlineMock.setWinningSolver(address(1)); - assertEq(flOnlineMock.getWinningSolver(), solverOneEOA, "winningSolver should still be solverOneEOA"); - } - - function testFLOnline_WithdrawRake() public { - deal(address(flOnlineMock), 1e18); - flOnlineMock.setRake(1e18); - uint256 rakeBefore = flOnlineMock.rake(); - uint256 callerBalanceBefore; - assertEq(rakeBefore, 1e18, "rake should start at 1 ETH"); - - callerBalanceBefore = address(userEOA).balance; - vm.prank(userEOA); - vm.expectRevert(FastLaneOnlineErrors.OuterHelpers_NotMadJustDisappointed.selector); - flOnlineMock.makeThogardsWifeHappy(); - - assertEq(flOnlineMock.rake(), rakeBefore, "rake should not change if withdraw fails"); - assertEq(address(userEOA).balance, callerBalanceBefore, "caller balance should not change if withdraw fails"); - - callerBalanceBefore = address(governanceEOA).balance; - vm.prank(governanceEOA); - flOnlineMock.makeThogardsWifeHappy(); - - assertEq(flOnlineMock.rake(), 0, "rake should be 0 after withdraw"); - assertEq(address(governanceEOA).balance, callerBalanceBefore + rakeBefore, "governance balance should increase"); - } - - function testFLOnline_BaselineEstablishedEvent() public { - _setUpUser(defaultSwapIntent); - _setUpSolver(solverOneEOA, solverOnePK, goodSolverBidETH); - - uint256 expectedBaselineAmountOut = _doBaselineCallWithChecksThenRevertChanges({ shouldSucceed: true }); - - vm.startPrank(userEOA); - vm.expectEmit(false, false, false, true, address(executionEnvironment)); - emit FastLaneOnlineInner.BaselineEstablished(defaultSwapIntent.minAmountUserBuys, expectedBaselineAmountOut); - (bool result,) = address(flOnline).call{ gas: args.gas + 1000, value: args.msgValue }( - abi.encodeCall(flOnline.fastOnlineSwap, (args.userOp)) - ); - assertTrue(result, "fastOnlineSwap should have succeeded"); - vm.stopPrank(); - } - - // ---------------------------------------------------- // - // Helpers // - // ---------------------------------------------------- // - - function _doFastOnlineSwapWithChecks( - address winningSolverEOA, - address winningSolver, - uint256 solverCount, - bool swapCallShouldSucceed - ) - internal - { - bool nativeTokenIn = args.swapIntent.tokenUserSells == NATIVE_TOKEN; - bool nativeTokenOut = args.swapIntent.tokenUserBuys == NATIVE_TOKEN; - bool solverWon = winningSolver != address(0); - - beforeVars.userTokenOutBalance = _balanceOf(args.swapIntent.tokenUserBuys, userEOA); - beforeVars.userTokenInBalance = _balanceOf(args.swapIntent.tokenUserSells, userEOA); - beforeVars.solverTokenOutBalance = _balanceOf(args.swapIntent.tokenUserBuys, winningSolver); - beforeVars.solverTokenInBalance = _balanceOf(args.swapIntent.tokenUserSells, winningSolver); - beforeVars.atlasGasSurcharge = atlas.cumulativeSurcharge(); - beforeVars.solverOneRep = flOnline.solverReputation(solverOneEOA); - beforeVars.solverTwoRep = flOnline.solverReputation(solverTwoEOA); - beforeVars.solverThreeRep = flOnline.solverReputation(solverThreeEOA); - - // adjust userTokenInBalance if native token - exclude gas treated as donation - if (nativeTokenIn) beforeVars.userTokenInBalance -= defaultMsgValue; - - uint256 txGasUsed; - uint256 estAtlasGasSurcharge = gasleft(); // Reused below during calculations - - // Do the actual fastOnlineSwap call - vm.prank(userEOA); - (bool result,) = address(flOnline).call{ gas: args.gas + 1000, value: args.msgValue }( - abi.encodeCall(flOnline.fastOnlineSwap, (args.userOp)) - ); - - // Calculate estimated Atlas gas surcharge taken from call above - txGasUsed = estAtlasGasSurcharge - gasleft(); - estAtlasGasSurcharge = txGasUsed * defaultGasPrice * atlas.ATLAS_SURCHARGE_RATE() / atlas.SCALE(); - - assertTrue( - result == swapCallShouldSucceed, - swapCallShouldSucceed ? "fastOnlineSwap should have succeeded" : "fastOnlineSwap should have reverted" - ); - - // Return early if transaction expected to revert. Balance checks below would otherwise fail. - if (!swapCallShouldSucceed) return; - - // Check Atlas gas surcharge earned is within 15% of the estimated gas surcharge - assertApproxEqRel( - atlas.cumulativeSurcharge() - beforeVars.atlasGasSurcharge, - estAtlasGasSurcharge, - ERR_MARGIN, - "Atlas gas surcharge not within estimated range" - ); - - // Check user's balances changed as expected - assertTrue( - _balanceOf(args.swapIntent.tokenUserBuys, userEOA) - >= beforeVars.userTokenOutBalance + args.swapIntent.minAmountUserBuys, - "User did not recieve enough tokenOut" - ); - - if (nativeTokenIn && solverWon) { - // Allow for small error margin due to gas refund from winning solver - uint256 buffer = 1e17; // 0.1 ETH buffer as base for error margin comparison - uint256 expectedBalanceAfter = beforeVars.userTokenInBalance - args.swapIntent.amountUserSells; - - assertApproxEqRel( - _balanceOf(args.swapIntent.tokenUserSells, userEOA) + buffer, - expectedBalanceAfter + buffer, - 0.01e18, // error marin: 1% of the 0.1 ETH buffer - "User did not send enough native tokenIn" - ); - } else { - assertEq( - _balanceOf(args.swapIntent.tokenUserSells, userEOA), - beforeVars.userTokenInBalance - args.swapIntent.amountUserSells, - "User did not send enough ERC20 tokenIn" - ); - } - - // If winning solver, check balances changed as expected - if (winningSolver != address(0)) { - assertTrue( - _balanceOf(args.swapIntent.tokenUserBuys, winningSolver) - <= beforeVars.solverTokenOutBalance - args.swapIntent.minAmountUserBuys, - "Solver did not send enough tokenOut" - ); - assertEq( - _balanceOf(args.swapIntent.tokenUserSells, winningSolver), - beforeVars.solverTokenInBalance + args.swapIntent.amountUserSells, - "Solver did not recieve enough tokenIn" - ); - } - - // Check reputation of all solvers involved - if (solverCount > 0) { - _checkReputationChanges({ - name: "solverOneEOA", - repBefore: beforeVars.solverOneRep, - repAfter: flOnline.solverReputation(solverOneEOA), - won: winningSolverEOA == solverOneEOA, - executionAttempted: attempted.solverOne - }); - } - if (solverCount > 1) { - _checkReputationChanges({ - name: "solverTwoEOA", - repBefore: beforeVars.solverTwoRep, - repAfter: flOnline.solverReputation(solverTwoEOA), - won: winningSolverEOA == solverTwoEOA, - executionAttempted: attempted.solverTwo - }); - } - if (solverCount > 2) { - _checkReputationChanges({ - name: "solverThreeEOA", - repBefore: beforeVars.solverThreeRep, - repAfter: flOnline.solverReputation(solverThreeEOA), - won: winningSolverEOA == solverThreeEOA, - executionAttempted: attempted.solverThree - }); - } - } - - // NOTE: This MUST be called at the start of each end-to-end test, to set up args - function _setUpUser(SwapIntent memory swapIntent) internal { - // always start with 0.01 ETH for gas/bundler fees - uint256 userStartNativeTokenBalance = 1e16; - - // Add tokens if user is selling native token - if (swapIntent.tokenUserSells == NATIVE_TOKEN) { - userStartNativeTokenBalance += swapIntent.amountUserSells; - } else { - // Otherwise deal user the ERC20 they are selling, and approve Atlas to take it - deal(swapIntent.tokenUserSells, userEOA, swapIntent.amountUserSells); - vm.prank(userEOA); - IERC20(swapIntent.tokenUserSells).approve(address(atlas), swapIntent.amountUserSells); - } - - // Burn all user's tokens they are buying, for clearer balance checks - // Exception: if user is buying native token, they still need some to pay gas - if (swapIntent.tokenUserBuys != NATIVE_TOKEN) { - deal(swapIntent.tokenUserBuys, userEOA, 0); - } - - // Give user the net amount of native token they need to start with - deal(userEOA, userStartNativeTokenBalance); - - // Build the other args data around the user's SwapIntent - args = _buildFastOnlineSwapArgs(swapIntent); - } - - function _buildFastOnlineSwapArgs(SwapIntent memory swapIntent) - internal - returns (FastOnlineSwapArgs memory newArgs) - { - bool nativeTokenIn = swapIntent.tokenUserSells == NATIVE_TOKEN; - newArgs.swapIntent = swapIntent; - newArgs.baselineCall = _buildBaselineCall(swapIntent, true); // should succeed - - (newArgs.userOp, newArgs.userOpHash) = flOnline.getUserOperationAndHash({ - swapper: userEOA, - swapIntent: newArgs.swapIntent, - baselineCall: newArgs.baselineCall, - deadline: defaultDeadlineBlock, - gas: defaultGasLimit, - maxFeePerGas: defaultGasPrice, - msgValue: nativeTokenIn ? swapIntent.amountUserSells : 0 - }); - - // User signs userOp - (sig.v, sig.r, sig.s) = vm.sign(userPK, newArgs.userOpHash); - newArgs.userOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - newArgs.deadline = defaultDeadlineBlock; - newArgs.gas = defaultGasLimit; - newArgs.maxFeePerGas = defaultGasPrice; - newArgs.msgValue = defaultMsgValue; - - // Add amountUserSells of ETH to the msg.value of the fastOnlineSwap call - if (nativeTokenIn) newArgs.msgValue += swapIntent.amountUserSells; - } - - function _buildBaselineCall( - SwapIntent memory swapIntent, - bool shouldSucceed - ) - internal - view - returns (BaselineCall memory) - { - bytes memory baselineData; - uint256 value; - uint256 amountOutMin = swapIntent.minAmountUserBuys; - address[] memory path = new address[](2); - path[0] = swapIntent.tokenUserSells; - path[1] = swapIntent.tokenUserBuys; - - // Make amountOutMin way too high to cause baseline call to fail - if (!shouldSucceed) amountOutMin *= 100; // 100x original amountOutMin - - if (swapIntent.tokenUserSells == NATIVE_TOKEN) { - path[0] = WETH_ADDRESS; - value = swapIntent.amountUserSells; - baselineData = abi.encodeCall( - routerV2.swapExactETHForTokens, - ( - amountOutMin, // amountOutMin - path, // path = [tokenUserSells, tokenUserBuys] - executionEnvironment, // to - defaultDeadlineTimestamp // deadline - ) - ); - } else if (swapIntent.tokenUserBuys == NATIVE_TOKEN) { - path[1] = WETH_ADDRESS; - baselineData = abi.encodeCall( - routerV2.swapExactTokensForETH, - ( - swapIntent.amountUserSells, // amountIn - amountOutMin, // amountOutMin - path, // path = [tokenUserSells, tokenUserBuys] - executionEnvironment, // to - defaultDeadlineTimestamp // deadline - ) - ); - } else { - baselineData = abi.encodeCall( - routerV2.swapExactTokensForTokens, - ( - swapIntent.amountUserSells, // amountIn - amountOutMin, // amountOutMin - path, // path = [tokenUserSells, tokenUserBuys] - executionEnvironment, // to - defaultDeadlineTimestamp // deadline - ) - ); - } - - return BaselineCall({ to: address(routerV2), data: baselineData, value: value }); - } - - function _setUpSolver(address solverEOA, uint256 solverPK, uint256 bidAmount) internal returns (address solver) { - (solver,) = _setUpSolver(solverEOA, solverPK, bidAmount, 0, bytes4(0)); - return solver; - } - - function _setUpSolver( - address solverEOA, - uint256 solverPK, - uint256 bidAmount, - uint256 congestionBuyIn - ) - internal - returns (SolverOperation memory solverOp) - { - (, solverOp) = _setUpSolver(solverEOA, solverPK, bidAmount, congestionBuyIn, bytes4(0)); - return solverOp; - } - - function _setUpSolver( - address solverEOA, - uint256 solverPK, - uint256 bidAmount, - bytes4 addSolverOpError - ) - internal - returns (address solver) - { - (solver,) = _setUpSolver(solverEOA, solverPK, bidAmount, 0, addSolverOpError); - return solver; - } - - function _setUpSolver( - address solverEOA, - uint256 solverPK, - uint256 bidAmount, - uint256 congestionBuyIn, - bytes4 addSolverOpError - ) - internal - returns (address solverContract, SolverOperation memory solverOp) - { - vm.startPrank(solverEOA); - // Make sure solver has 1 AtlETH bonded in Atlas - uint256 bonded = atlas.balanceOfBonded(solverEOA); - if (bonded < 1e18) { - uint256 atlETHBalance = atlas.balanceOf(solverEOA); - if (atlETHBalance < 1e18) { - deal(solverEOA, 1e18 - atlETHBalance); - atlas.deposit{ value: 1e18 - atlETHBalance }(); - } - atlas.bond(1e18 - bonded); - } - - // Deploy RFQ solver contract - bytes32 salt = keccak256(abi.encodePacked(address(flOnline), solverEOA, bidAmount, vm.getNonce(solverEOA))); - FLOnlineRFQSolver solver = new FLOnlineRFQSolver{ salt: salt }(WETH_ADDRESS, address(atlas)); - - // Create signed solverOp - solverOp = _buildSolverOp(solverEOA, solverPK, address(solver), bidAmount); - - // Give solver contract enough tokenOut to fulfill user's SwapIntent - if (args.swapIntent.tokenUserBuys != NATIVE_TOKEN) { - deal(args.swapIntent.tokenUserBuys, address(solver), bidAmount); - } else { - deal(address(solver), bidAmount); - } - if (congestionBuyIn > 0) deal(solverEOA, congestionBuyIn); - - // Register solverOp in FLOnline in frontrunning tx - if (addSolverOpError != bytes4(0)) vm.expectRevert(addSolverOpError); - flOnline.addSolverOp{ value: congestionBuyIn }({ userOp: args.userOp, solverOp: solverOp }); - - // Return early if addSolverOp expected to revert - if (addSolverOpError != bytes4(0)) return (address(0), solverOp); - vm.stopPrank(); - - if (solverEOA == solverOneEOA) attempted.solverOne = true; - if (solverEOA == solverTwoEOA) attempted.solverTwo = true; - if (solverEOA == solverThreeEOA) attempted.solverThree = true; - - // Returns the address of the solver contract deployed here - return (address(solver), solverOp); - } - - function _buildSolverOp( - address solverEOA, - uint256 solverPK, - address solverContract, - uint256 bidAmount - ) - internal - returns (SolverOperation memory solverOp) - { - solverOp = SolverOperation({ - from: solverEOA, - to: address(atlas), - value: 0, - gas: flOnline.MAX_SOLVER_GAS() - 1, - maxFeePerGas: defaultGasPrice, - deadline: defaultDeadlineBlock, - solver: solverContract, - control: address(flOnline), - userOpHash: args.userOpHash, - bidToken: args.swapIntent.tokenUserBuys, - bidAmount: bidAmount, - data: abi.encodeCall(FLOnlineRFQSolver.fulfillRFQ, (args.swapIntent)), - signature: new bytes(0) - }); - // Sign solverOp - (sig.v, sig.r, sig.s) = vm.sign(solverPK, atlasVerification.getSolverPayload(solverOp)); - solverOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - } - - // Returns the amount of tokensOut received, which forms the baseline solvers should beat - function _doBaselineCallWithChecksThenRevertChanges(bool shouldSucceed) internal returns (uint256) { - uint256 snapshotId = vm.snapshot(); // Everything below this gets reverted after function ends - - uint256 eeTokenInBefore = _balanceOf(args.swapIntent.tokenUserSells, executionEnvironment); - uint256 eeTokenOutBefore = _balanceOf(args.swapIntent.tokenUserBuys, executionEnvironment); - uint256 eeTokenOutAfter; - - if (eeTokenInBefore < args.swapIntent.amountUserSells) { - if (args.swapIntent.tokenUserSells == NATIVE_TOKEN) { - deal(executionEnvironment, args.swapIntent.amountUserSells); - } else { - deal(args.swapIntent.tokenUserSells, executionEnvironment, args.swapIntent.amountUserSells); - } - - eeTokenInBefore = _balanceOf(args.swapIntent.tokenUserSells, executionEnvironment); - } - - bool success; - vm.startPrank(executionEnvironment); - - if (args.swapIntent.tokenUserSells != NATIVE_TOKEN) { - IERC20(args.swapIntent.tokenUserSells).approve(args.baselineCall.to, args.swapIntent.amountUserSells); - (success,) = args.baselineCall.to.call(args.baselineCall.data); - } else { - (success,) = args.baselineCall.to.call{ value: args.swapIntent.amountUserSells }(args.baselineCall.data); - } - - vm.stopPrank(); - - assertTrue( - success == shouldSucceed, - shouldSucceed ? "Baseline call should have succeeded" : "Baseline call should have reverted" - ); - - if (!shouldSucceed) { - vm.revertTo(snapshotId); - return 0; - } - - eeTokenOutAfter = _balanceOf(args.swapIntent.tokenUserBuys, executionEnvironment); - assertTrue( - eeTokenOutAfter >= eeTokenOutBefore + args.swapIntent.minAmountUserBuys, - "EE did not recieve expected tokenOut in baseline call" - ); - assertEq( - _balanceOf(args.swapIntent.tokenUserSells, executionEnvironment), - eeTokenInBefore - args.swapIntent.amountUserSells, - "EE did not send expected tokenIn in baseline call" - ); - //Revert back to state before baseline call was done - vm.revertTo(snapshotId); - return eeTokenOutAfter - eeTokenOutBefore; - } - - function _setBaselineCallToRevert() internal { - // should not succeed - args.baselineCall = _buildBaselineCall(args.swapIntent, false); - - // Need to update the userOp with changes to baseline call - (args.userOp, args.userOpHash) = flOnline.getUserOperationAndHash({ - swapper: userEOA, - swapIntent: args.swapIntent, - baselineCall: args.baselineCall, - deadline: defaultDeadlineBlock, - gas: defaultGasLimit, - maxFeePerGas: defaultGasPrice, - msgValue: 0 - }); - - // User signs userOp - (sig.v, sig.r, sig.s) = vm.sign(userPK, args.userOpHash); - args.userOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - } - - // Checks the solverOps array is sorted in descending order of bidAmount - function _isSolverOpsSorted(SolverOperation[] memory solverOps) internal pure returns (bool) { - if (solverOps.length < 2) return true; - for (uint256 i = 0; i < solverOps.length - 1; i++) { - if (solverOps[i].bidAmount < solverOps[i + 1].bidAmount) { - return false; - } - } - return true; - } - - function _checkReputationChanges( - string memory name, - Reputation memory repBefore, - Reputation memory repAfter, - bool won, - bool executionAttempted - ) - internal - pure - { - if (executionAttempted) { - if (won) { - assertGt( - repAfter.successCost, - repBefore.successCost, - string.concat(name, " successCost not updated correctly") - ); - assertEq( - repAfter.failureCost, - repBefore.failureCost, - string.concat(name, " failureCost should not have changed") - ); - } else { - assertGt( - repAfter.failureCost, - repBefore.failureCost, - string.concat(name, " failureCost not updated correctly") - ); - assertEq( - repAfter.successCost, - repBefore.successCost, - string.concat(name, " successCost should not have changed") - ); - } - } else { - // not attempted due to not being included in solverOps, or due to having a lower bid than the winning - // solver and thus a higher index in the sorted array. No change in reputation expected. - assertEq( - repAfter.successCost, repBefore.successCost, string.concat(name, " successCost should not have changed") - ); - assertEq( - repAfter.failureCost, repBefore.failureCost, string.concat(name, " failureCost should not have changed") - ); - } - } - - // balanceOf helper that supports ERC20 and native token - function _balanceOf(address token, address account) internal view returns (uint256) { - if (token == NATIVE_TOKEN) { - return account.balance; - } else { - return IERC20(token).balanceOf(account); - } - } - - function sqrt(uint256 y) internal pure returns (uint256 z) { - if (y > 3) { - z = y; - uint256 x = y / 2 + 1; - while (x < z) { - z = x; - x = (y / x + x) / 2; - } - } else if (y != 0) { - z = 1; - } - } -} - -// This solver magically has the tokens needed to fulfil the user's swap. -// This might involve an offchain RFQ system -contract FLOnlineRFQSolver is SolverBase { - address internal constant NATIVE_TOKEN = address(0); - bool internal s_shouldSucceed; - - constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { - s_shouldSucceed = true; // should succeed by default, can be set to false - } - - function shouldSucceed() public view returns (bool) { - return s_shouldSucceed; - } - - function setShouldSucceed(bool succeed) public { - s_shouldSucceed = succeed; - } - - function fulfillRFQ(SwapIntent calldata swapIntent) public view onlySelf { - require(s_shouldSucceed, "Solver failed intentionally"); - - if (swapIntent.tokenUserSells == NATIVE_TOKEN) { - require( - address(this).balance >= swapIntent.amountUserSells, "Did not receive expected amount of tokenUserBuys" - ); - } else { - require( - IERC20(swapIntent.tokenUserSells).balanceOf(address(this)) >= swapIntent.amountUserSells, - "Did not receive expected amount of tokenUserSells" - ); - } - // The solver bid representing user's minAmountUserBuys of tokenUserBuys is sent to the - // Execution Environment in the payBids modifier logic which runs after this function ends. - } - - // This ensures a function can only be called through atlasSolverCall - // which includes security checks to work safely with Atlas - modifier onlySelf() { - require(msg.sender == address(this), "Not called via atlasSolverCall"); - _; - } - - fallback() external payable { } - receive() external payable { } -} - -// Mock contract to expose internal FLOnline functions for unit testing -contract MockFastLaneOnline is FastLaneOnlineOuter { - constructor(address atlas, address protocolGuildWallet) FastLaneOnlineOuter(atlas, protocolGuildWallet) { } - - // ---------------------------------------------------- // - // OuterHelpers.sol // - // ---------------------------------------------------- // - - function sortSolverOps(SolverOperation[] memory solverOps) external pure returns (SolverOperation[] memory) { - return _sortSolverOps(solverOps); - } - - function processCongestionRake( - uint256 startingBalance, - bytes32 userOpHash, - bool solversSuccessful - ) - external - returns (uint256 netGasRefund) - { - netGasRefund = _processCongestionRake(startingBalance, userOpHash, solversSuccessful); - } - - function setAggCongestionBuyIn(bytes32 userOpHash, uint256 newAggCongestionBuyIn) external { - S_aggCongestionBuyIn[userOpHash] = newAggCongestionBuyIn; - } - - function setRake(uint256 newRake) external { - S_rake = newRake; - } - - // ---------------------------------------------------- // - // SolverGateway.sol // - // ---------------------------------------------------- // - - function SLIPPAGE_BASE() external pure returns (uint256) { - return _SLIPPAGE_BASE; - } - - function GLOBAL_MAX_SLIPPAGE() external pure returns (uint256) { - return _GLOBAL_MAX_SLIPPAGE; - } - - function calculateBidFactor(uint256 bidAmount, uint256 minAmountUserBuys) external pure returns (uint256) { - return _calculateBidFactor(bidAmount, minAmountUserBuys); - } - - function calculateWeightedScore( - uint256 totalGas, - uint256 solverOpGas, - uint256 maxFeePerGas, - uint256 congestionBuyIn, - uint256 solverCount, - uint256 bidFactor, - Reputation memory rep - ) - external - pure - returns (uint256) - { - return - _calculateWeightedScore(totalGas, solverOpGas, maxFeePerGas, congestionBuyIn, solverCount, bidFactor, rep); - } - - function pushSolverOp(bytes32 userOpHash, bytes32 solverOpHash) external { - _pushSolverOp(userOpHash, solverOpHash); - } - - function replaceSolverOp(bytes32 userOpHash, bytes32 solverOpHash, uint256 replacedIndex) external { - _replaceSolverOp(userOpHash, solverOpHash, replacedIndex); - } - - function evaluateForInclusion( - UserOperation calldata userOp, - SolverOperation calldata solverOp - ) - external - view - returns (bool pushAsNew, bool replaceExisting, uint256) - { - return _evaluateForInclusion(userOp, solverOp); - } - - // ---------------------------------------------------- // - // BaseStorage.sol // - // ---------------------------------------------------- // - - function getUserLock() external view returns (address) { - return _getUserLock(); - } - - function setUserLock(address user) external { - _setUserLock(user); - } - - function getWinningSolver() external view returns (address) { - return _getWinningSolver(); - } -} diff --git a/test/OEV.t.sol b/test/OEV.t.sol deleted file mode 100644 index 58a57fdc2..000000000 --- a/test/OEV.t.sol +++ /dev/null @@ -1,746 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import "forge-std/Test.sol"; - -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; - -import { BaseTest } from "test/base/BaseTest.t.sol"; -import { TxBuilder } from "src/contracts/helpers/TxBuilder.sol"; -import { UserOperationBuilder } from "test/base/builders/UserOperationBuilder.sol"; - -import { SolverOperation } from "src/contracts/types/SolverOperation.sol"; -import { UserOperation } from "src/contracts/types/UserOperation.sol"; -import { DAppConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/DAppOperation.sol"; - -import { ChainlinkDAppControl, Oracle, Role } from "src/contracts/examples/oev-example/ChainlinkDAppControl.sol"; -import { ChainlinkAtlasWrapper } from "src/contracts/examples/oev-example/ChainlinkAtlasWrapper.sol"; -import { AggregatorV2V3Interface } from "src/contracts/examples/oev-example/IChainlinkAtlasWrapper.sol"; -import { SolverBase } from "src/contracts/solver/SolverBase.sol"; - - -// Using this Chainlink update to ETHUSD feed as an example: -// Aggregator: https://etherscan.io/address/0xE62B71cf983019BFf55bC83B48601ce8419650CC -// Transmit tx: https://etherscan.io/tx/0x3645d1bc223efe0861e02aeb95d6204c5ebfe268b64a7d23d385520faf452bc0 -// ETH/USD set to: $2941.02 == 294102000000 - -contract OEVTest is BaseTest { - ChainlinkAtlasWrapper public chainlinkAtlasWrapper; - ChainlinkDAppControl public chainlinkDAppControl; - MockLiquidatable public mockLiquidatable; - TxBuilder public txBuilder; - Sig public sig; - - address chainlinkGovEOA; - address aaveGovEOA; - address executionEnvironment; - - address chainlinkETHUSD = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; - uint256 forkBlock = 19289829; // Block just before the transmit tx above - uint256 targetOracleAnswer = 294102000000; - uint256 liquidationReward = 10e18; - uint256 solverWinningBid = 1e18; - - struct TransmitData { - bytes report; - bytes32[] rs; - bytes32[] ss; - bytes32 rawVs; - } - - function setUp() public virtual override { - BaseTest.setUp(); - vm.rollFork(forkBlock); - vm.deal(solverOneEOA, 100e18); - - // Creating new gov address (SignatoryActive error if already registered with control) - uint256 chainlinkGovPK = 11_112; - uint256 aaveGovPK = 11_113; - chainlinkGovEOA = vm.addr(chainlinkGovPK); - aaveGovEOA = vm.addr(aaveGovPK); - - vm.startPrank(chainlinkGovEOA); - // Chainlink's Gov address deploys the Chainlink DAppControl - chainlinkDAppControl = new ChainlinkDAppControl(address(atlas)); - // Chainlink's Gov address initializes the Chainlink DAppControl in Atlas - atlasVerification.initializeGovernance(address(chainlinkDAppControl)); - // Set Chainlink's ETHUSD feed signers in DAppControl for verification - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, getETHUSDSigners()); - vm.stopPrank(); - - vm.startPrank(userEOA); // User is a Chainlink Node - executionEnvironment = atlas.createExecutionEnvironment(userEOA, address(chainlinkDAppControl)); - vm.stopPrank(); - - vm.startPrank(aaveGovEOA); - // Aave creates a Chainlink Atlas Wrapper for ETH/USD to capture OEV - chainlinkAtlasWrapper = ChainlinkAtlasWrapper(payable(chainlinkDAppControl.createNewChainlinkAtlasWrapper(chainlinkETHUSD))); - // OEV-generating protocols must use the Chainlink Atlas Wrapper for price feed in order to capture the OEV - mockLiquidatable = new MockLiquidatable(address(chainlinkAtlasWrapper), targetOracleAnswer); - // Aave sets the Chainlink Execution Environment as a trusted transmitter in the Chainlink Atlas Wrapper - chainlinkAtlasWrapper.setTransmitterStatus(executionEnvironment, true); - vm.stopPrank(); - - deal(address(mockLiquidatable), liquidationReward); // Add 10 ETH as liquidation reward - - txBuilder = new TxBuilder({ - _control: address(chainlinkDAppControl), - _atlas: address(atlas), - _verification: address(atlasVerification) - }); - - vm.label(chainlinkGovEOA, "Chainlink Gov"); - vm.label(aaveGovEOA, "Aave Gov"); - vm.label(address(executionEnvironment), "EXECUTION ENV"); - vm.label(address(chainlinkAtlasWrapper), "Chainlink Atlas Wrapper"); - vm.label(address(chainlinkDAppControl), "Chainlink DApp Control"); - vm.label(address(chainlinkETHUSD), "Chainlink Base ETH/USD Feed"); - } - - // ---------------------------------------------------- // - // Full OEV Capture Test // - // ---------------------------------------------------- // - - function testChainlinkOEV_StandardVersion_GasCheck_SkipCoverage() public { - UserOperation memory userOp; - SolverOperation[] memory solverOps = new SolverOperation[](1); - DAppOperation memory dAppOp; - - vm.startPrank(solverOneEOA); - LiquidationOEVSolver liquidationSolver = new LiquidationOEVSolver(WETH_ADDRESS, address(atlas)); - atlas.deposit{ value: 1e18 }(); - atlas.bond(1e18); - vm.stopPrank(); - - assertTrue(chainlinkDAppControl.isChainlinkWrapper(address(chainlinkAtlasWrapper)), "Wrapper should be registered on DAppControl - otherwise will revert in allocateValue"); - - // Basic userOp created but excludes oracle price update data - userOp = txBuilder.buildUserOperation({ - from: userEOA, - to: address(chainlinkAtlasWrapper), // Aave's ChainlinkAtlasWrapper for ETHUSD - maxFeePerGas: tx.gasprice + 1, - value: 0, - deadline: block.number + 2, - data: "" // No userOp.data yet - only created after solverOps are signed - }); - userOp.sessionKey = governanceEOA; - - bytes memory solverOpData = - abi.encodeWithSelector(LiquidationOEVSolver.liquidate.selector, address(mockLiquidatable)); - - solverOps[0] = txBuilder.buildSolverOperation({ - userOp: userOp, - solverOpData: solverOpData, - solver: solverOneEOA, - solverContract: address(liquidationSolver), - bidAmount: solverWinningBid, - value: 0 - }); - - (sig.v, sig.r, sig.s) = vm.sign(solverOnePK, atlasVerification.getSolverPayload(solverOps[0])); - solverOps[0].signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // After solvers have signed their ops, Chainlink creates the userOp with price update data - (bytes memory report, bytes32[] memory rs, bytes32[] memory ss, bytes32 rawVs) = getTransmitPayload(); - userOp.data = abi.encodeWithSelector(ChainlinkAtlasWrapper.transmit.selector, report, rs, ss, rawVs); - - dAppOp = txBuilder.buildDAppOperation(governanceEOA, userOp, solverOps); - (sig.v, sig.r, sig.s) = vm.sign(governancePK, atlasVerification.getDAppOperationPayload(dAppOp)); - dAppOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - assertEq(mockLiquidatable.canLiquidate(), false); - assertTrue(uint(chainlinkAtlasWrapper.latestAnswer()) != targetOracleAnswer, "Wrapper answer should not be target yet"); - assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), uint(AggregatorV2V3Interface(chainlinkETHUSD).latestAnswer()), "Wrapper and base feed should report same answer"); - assertEq(address(chainlinkAtlasWrapper).balance, 0, "Wrapper should not have any ETH"); - - // To show the signer verification checks cause metacall to pass/fail: - uint256 snapshot = vm.snapshot(); - - // 3rd signer in the ETHUSD transmit example tx used - address signerToRemove = 0xCc1b49B86F79C7E50E294D3e3734fe94DB9A42F0; - vm.prank(chainlinkGovEOA); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, signerToRemove); - - // Should Fail - vm.prank(userEOA); - atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp }); - - assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), uint(AggregatorV2V3Interface(chainlinkETHUSD).latestAnswer()), "Metacall unexpectedly succeeded"); - - // Go back to before removing the signer - vm.revertTo(snapshot); - - uint256 gasLeftBefore = gasleft(); - - // Should Succeed - vm.prank(userEOA); - atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp }); - - console.log("Metacall Gas Cost:", gasLeftBefore - gasleft()); - - assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), targetOracleAnswer, "Wrapper did not update as expected"); - assertTrue(uint(chainlinkAtlasWrapper.latestAnswer()) != uint(AggregatorV2V3Interface(chainlinkETHUSD).latestAnswer()), "Wrapper and base feed should report different answers"); - assertEq(address(chainlinkAtlasWrapper).balance, solverWinningBid, "Wrapper should hold winning bid as OEV"); - } - - // ---------------------------------------------------- // - // ChainlinkAtlasWrapper Tests // - // ---------------------------------------------------- // - - function testChainlinkAtlasWrapperViewFunctions() public { - // Check wrapper and base start as expected - assertEq(chainlinkAtlasWrapper.atlasLatestAnswer(), 0, "Wrapper stored latestAnswer should be 0"); - assertTrue(AggregatorV2V3Interface(chainlinkETHUSD).latestAnswer() != 0, "Base latestAnswer should not be 0"); - assertEq(chainlinkAtlasWrapper.atlasLatestTimestamp(), 0, "Wrapper stored latestTimestamp should be 0"); - assertTrue(AggregatorV2V3Interface(chainlinkETHUSD).latestTimestamp() != 0, "Base latestTimestamp should not be 0"); - - (uint80 roundIdAtlas, int256 answerAtlas, uint256 startedAtAtlas, uint256 updatedAtAtlas, uint80 answeredInRoundAtlas) = chainlinkAtlasWrapper.latestRoundData(); - (uint80 roundIdBase, int256 answerBase, uint256 startedAtBase, uint256 updatedAtBase, uint80 answeredInRoundBase) = AggregatorV2V3Interface(chainlinkETHUSD).latestRoundData(); - - // Before Atlas price update, all view functions should fall back to base oracle - assertEq(chainlinkAtlasWrapper.latestAnswer(), AggregatorV2V3Interface(chainlinkETHUSD).latestAnswer(), "latestAnswer should be same as base"); - assertEq(chainlinkAtlasWrapper.latestTimestamp(), AggregatorV2V3Interface(chainlinkETHUSD).latestTimestamp(), "latestTimestamp should be same as base"); - assertEq(roundIdAtlas, roundIdBase, "roundId should be same as base"); - assertEq(answerAtlas, answerBase, "answer should be same as base"); - assertEq(startedAtAtlas, startedAtBase, "startedAt should be same as base"); - assertEq(updatedAtAtlas, updatedAtBase, "updatedAt should be same as base"); - assertEq(answeredInRoundAtlas, answeredInRoundBase, "answeredInRound should be same as base"); - - // Update wrapper with new price by calling transmit from an approved EE - TransmitData memory transmitData; - (transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs) = getTransmitPayload(); - vm.prank(executionEnvironment); - chainlinkAtlasWrapper.transmit(transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs); - - // After Atlas price update, latestAnswer and latestTimestamp should be different to base oracle - assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), targetOracleAnswer, "latestAnswer should be updated"); - assertTrue(uint(chainlinkAtlasWrapper.latestAnswer()) != uint(AggregatorV2V3Interface(chainlinkETHUSD).latestAnswer()), "latestAnswer should be different to base"); - assertEq(chainlinkAtlasWrapper.latestTimestamp(), block.timestamp, "latestTimestamp should be updated"); - assertTrue(chainlinkAtlasWrapper.latestTimestamp() > AggregatorV2V3Interface(chainlinkETHUSD).latestTimestamp(), "latestTimestamp should be later than base"); - - (roundIdAtlas, answerAtlas, startedAtAtlas, updatedAtAtlas, answeredInRoundAtlas) = chainlinkAtlasWrapper.latestRoundData(); - (roundIdBase, answerBase, startedAtBase, updatedAtBase, answeredInRoundBase) = AggregatorV2V3Interface(chainlinkETHUSD).latestRoundData(); - - assertEq(roundIdAtlas, roundIdBase, "roundId should still be same as base"); - assertTrue(answerAtlas == int(targetOracleAnswer) && answerAtlas != answerBase, "answer should be updated"); - assertEq(startedAtAtlas, startedAtBase, "startedAt should still be same as base"); - assertTrue(updatedAtAtlas > updatedAtBase, "updatedAt should be later than base"); - assertEq(answeredInRoundAtlas, answeredInRoundBase, "answeredInRound should still be same as base"); - } - - function testChainlinkAtlasWrapperWithdrawFunctions() public { - uint256 startETH = 10e18; - uint256 startDai = 5e18; - deal(address(chainlinkAtlasWrapper), startETH); // Give wrapper 10 ETH - deal(address(DAI), address(chainlinkAtlasWrapper), startDai); // Give wrapper 5 DAI - - assertEq(address(chainlinkAtlasWrapper).balance, startETH, "Wrapper should have 10 ETH"); - assertEq(DAI.balanceOf(address(chainlinkAtlasWrapper)), startDai, "Wrapper should have 5 DAI"); - assertEq(aaveGovEOA.balance, 0, "Aave Gov should have 0 ETH"); - assertEq(DAI.balanceOf(aaveGovEOA), 0, "Aave Gov should have 0 DAI"); - - vm.startPrank(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, chainlinkGovEOA)); - chainlinkAtlasWrapper.withdrawETH(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, chainlinkGovEOA)); - chainlinkAtlasWrapper.withdrawERC20(address(DAI), chainlinkGovEOA); - vm.stopPrank(); - - assertEq(aaveGovEOA.balance, 0, "Aave Gov should still have 0 ETH"); - assertEq(DAI.balanceOf(aaveGovEOA), 0, "Aave Gov should still have 0 DAI"); - - vm.startPrank(aaveGovEOA); - chainlinkAtlasWrapper.withdrawETH(aaveGovEOA); - chainlinkAtlasWrapper.withdrawERC20(address(DAI), aaveGovEOA); - vm.stopPrank(); - - assertEq(address(chainlinkAtlasWrapper).balance, 0, "Wrapper should have 0 ETH"); - assertEq(DAI.balanceOf(address(chainlinkAtlasWrapper)), 0, "Wrapper should have 0 DAI"); - assertEq(aaveGovEOA.balance, startETH, "Aave Gov should have 10 ETH"); - assertEq(DAI.balanceOf(aaveGovEOA), startDai, "Aave Gov should have 5 DAI"); - } - - function testChainlinkAtlasWrapperOwnableFunctionsEvents() public { - address mockEE = makeAddr("Mock EE"); - - // Wrapper emits event on deployment to show ownership transfer - vm.expectEmit(true, false, false, true); - emit Ownable.OwnershipTransferred(address(0), address(chainlinkAtlasWrapper.owner())); - new ChainlinkAtlasWrapper(address(atlas), chainlinkETHUSD, aaveGovEOA); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, chainlinkGovEOA)); - chainlinkAtlasWrapper.setTransmitterStatus(mockEE, true); - - assertEq(chainlinkAtlasWrapper.transmitters(mockEE), false, "EE should not be trusted yet"); - - vm.prank(aaveGovEOA); - chainlinkAtlasWrapper.setTransmitterStatus(mockEE, true); - - assertEq(chainlinkAtlasWrapper.transmitters(mockEE), true, "EE should be trusted now"); - } - - function testChainlinkAtlasWrapperTransmit() public { - TransmitData memory transmitData; - (transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs) = getTransmitPayload(); - - assertEq(chainlinkAtlasWrapper.atlasLatestAnswer(), 0, "Wrapper stored latestAnswer should be 0"); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(ChainlinkAtlasWrapper.TransmitterNotTrusted.selector, chainlinkGovEOA)); - chainlinkAtlasWrapper.transmit(transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs); - - assertEq(chainlinkAtlasWrapper.atlasLatestAnswer(), 0, "Wrapper stored latestAnswer should still be 0"); - - // Check transmit reverts if rs and ss arrays are not the same length - vm.prank(executionEnvironment); - vm.expectRevert(ChainlinkAtlasWrapper.ArrayLengthMismatch.selector); - chainlinkAtlasWrapper.transmit(transmitData.report, new bytes32[](0), transmitData.ss, transmitData.rawVs); - - // Check that transmit reverts if quorum is not met (uses rs array length for sig count) - vm.prank(executionEnvironment); - vm.expectRevert(ChainlinkAtlasWrapper.SignerVerificationFailed.selector); - chainlinkAtlasWrapper.transmit(transmitData.report, new bytes32[](0), new bytes32[](0), transmitData.rawVs); - - vm.prank(executionEnvironment); - chainlinkAtlasWrapper.transmit(transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs); - - assertEq(uint(chainlinkAtlasWrapper.atlasLatestAnswer()), targetOracleAnswer, "Wrapper stored latestAnswer should be updated"); - - // Check that transmit reverts if called again with same data - vm.prank(executionEnvironment); - vm.expectRevert(ChainlinkAtlasWrapper.CannotReuseReport.selector); - chainlinkAtlasWrapper.transmit(transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs); - } - - function testChainlinkAtlasWrapperCanReceiveETH() public { - deal(userEOA, 2e18); - - assertEq(address(chainlinkAtlasWrapper).balance, 0, "Wrapper should have 0 ETH"); - - vm.startPrank(userEOA); - payable(address(chainlinkAtlasWrapper)).transfer(1e18); - (bool success, ) = address(chainlinkAtlasWrapper).call{value: 1e18}(""); - vm.stopPrank(); - - assertTrue(success, "Transfer should succeed"); - assertEq(address(chainlinkAtlasWrapper).balance, 2e18, "Wrapper should have 2 ETH"); - } - - // ---------------------------------------------------- // - // ChainlinkDAppControl Tests // - // ---------------------------------------------------- // - - function test_ChainlinkDAppControl_setSignersForBaseFeed() public { - address[] memory signers = getETHUSDSigners(); - address[] memory signersFromDAppControl; - - vm.expectRevert(ChainlinkDAppControl.OnlyGovernance.selector); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, signers); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignersSetForBaseFeed(chainlinkETHUSD, signers); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, signers); - - signersFromDAppControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - assertEq(signersFromDAppControl.length, signers.length, "Signers length should be same as expected"); - for (uint i = 0; i < signers.length; i++) { - assertEq(signersFromDAppControl[i], signers[i], "Signer should be same as expected"); - } - - address[] memory blankSigners = new address[](0); - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignersSetForBaseFeed(chainlinkETHUSD, blankSigners); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, blankSigners); - - assertEq(chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD).length, 0, "Signers should be empty"); - - // Should revert on too many signers - address[] memory tooManySigners = new address[](chainlinkDAppControl.MAX_NUM_ORACLES() + 1); - for (uint i = 0; i < signers.length; i++) { - tooManySigners[i] = signers[i]; - } - tooManySigners[chainlinkDAppControl.MAX_NUM_ORACLES()] = chainlinkGovEOA; - - vm.prank(chainlinkGovEOA); - vm.expectRevert(ChainlinkDAppControl.TooManySigners.selector); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, tooManySigners); - - // Should revert on duplicate signers in the array - address[] memory duplicateSigners = new address[](2); - duplicateSigners[0] = chainlinkGovEOA; - duplicateSigners[1] = chainlinkGovEOA; - - vm.prank(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(ChainlinkDAppControl.DuplicateSigner.selector, chainlinkGovEOA)); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, duplicateSigners); - - // Check properties set correctly on valid signer set - address[] memory validSigners = new address[](2); - validSigners[0] = chainlinkGovEOA; - validSigners[1] = aaveGovEOA; - - Oracle memory oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - assertEq(uint(oracle.role), uint(Role.Unset), "Oracle role should be Unset"); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignersSetForBaseFeed(chainlinkETHUSD, validSigners); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, validSigners); - - address[] memory signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - assertEq(signersFromDappControl.length, validSigners.length, "length mismatch"); - assertEq(signersFromDappControl[0], validSigners[0]); - assertEq(signersFromDappControl[1], validSigners[1]); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - assertEq(oracle.index, 0, "Oracle index should be 0"); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, aaveGovEOA); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - assertEq(oracle.index, 1, "Oracle index should be 1"); - } - - function test_ChainlinkDAppControl_addSignerForBaseFeed() public { - vm.expectRevert(ChainlinkDAppControl.OnlyGovernance.selector); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - - address[] memory signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - assertEq(signersFromDappControl.length, chainlinkDAppControl.MAX_NUM_ORACLES(), "Should have max signers"); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(ChainlinkDAppControl.TooManySigners.selector); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - - // clear signers so we can add individually - address[] memory blankSigners = new address[](0); - vm.prank(chainlinkGovEOA); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, blankSigners); - assertEq(chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD).length, 0, "Signers should be empty"); - - Oracle memory oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - assertEq(uint(oracle.role), uint(Role.Unset), "Oracle role should be Unset"); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignerAddedForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - - signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - assertEq(signersFromDappControl.length, 1, "Signers should have 1"); - assertEq(signersFromDappControl[0], chainlinkGovEOA, "Signer should be chainlinkGovEOA"); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(ChainlinkDAppControl.DuplicateSigner.selector, chainlinkGovEOA)); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, address(1)); - assertEq(uint(oracle.role), uint(Role.Unset), "Oracle role should be Unset"); - assertEq(oracle.index, 0, "Oracle index should be 0"); - - vm.prank(chainlinkGovEOA); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, address(1)); - - signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, address(1)); - assertEq(signersFromDappControl.length, 2, "Signers should have 2"); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - assertEq(oracle.index, 1, "Oracle index should be 1"); - } - - function test_ChainlinkDAppControl_removeSignerOfBaseFeed() public { - address[] memory realSigners = getETHUSDSigners(); - address signerToRemove = realSigners[10]; - address[] memory signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - Oracle memory oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, signerToRemove); - assertEq(signersFromDappControl.length, 31, "Should have 31 signers at start"); - assertEq(signersFromDappControl[10], realSigners[10], "targeted signer still registered"); - assertEq(oracle.index, 10); - assertEq(uint(oracle.role), uint(Role.Signer)); - - vm.expectRevert(ChainlinkDAppControl.OnlyGovernance.selector); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, realSigners[30]); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(ChainlinkDAppControl.SignerNotFound.selector); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, address(1)); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignerRemovedForBaseFeed(chainlinkETHUSD, signerToRemove); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, signerToRemove); - - signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, signerToRemove); - assertEq(signersFromDappControl.length, realSigners.length - 1, "Should have 30 signers now"); - assertTrue(signersFromDappControl[10] != realSigners[10], "Idx 10 signers should be diff now"); - assertEq(oracle.index, 0); - assertEq(uint(oracle.role), uint(Role.Unset)); - } - - function test_ChainlinkDAppControl_setObservationsQuorum() public { - uint256 expectedDefaultQuorum = 1; - uint256 newQuorum = 3; - - assertEq(chainlinkDAppControl.observationsQuorum(), expectedDefaultQuorum, "Default starting observations quorum not as expected"); - - vm.expectRevert(ChainlinkDAppControl.OnlyGovernance.selector); - chainlinkDAppControl.setObservationsQuorum(newQuorum); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.ObservationsQuorumSet(expectedDefaultQuorum, newQuorum); - chainlinkDAppControl.setObservationsQuorum(newQuorum); - - assertEq(chainlinkDAppControl.observationsQuorum(), newQuorum, "Observations quorum not updated as expected"); - } - - function test_ChainlinkDAppControl_verifyTransmitSigners() public { - // 3rd signer in the ETHUSD transmit example tx used - address signerToRemove = 0xCc1b49B86F79C7E50E294D3e3734fe94DB9A42F0; - ( - bytes memory report, bytes32[] memory rs, - bytes32[] memory ss, bytes32 rawVs - ) = getTransmitPayload(); - - // All signers should be verified - assertEq(chainlinkDAppControl.verifyTransmitSigners(chainlinkETHUSD, report, rs, ss, rawVs), true); - - // If quorum is not met, should return false - uint256 quorum = chainlinkDAppControl.observationsQuorum(); - require(quorum > 0, "TEST: Quorum cant be 0 for this check"); - assertEq(chainlinkDAppControl.verifyTransmitSigners(chainlinkETHUSD, report, new bytes32[](0), ss, rawVs), false); - - // If a verified signer is removed from DAppControl, should return false - vm.prank(chainlinkGovEOA); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, signerToRemove); - assertEq(chainlinkDAppControl.verifyTransmitSigners(chainlinkETHUSD, report, rs, ss, rawVs), false); - } - - function test_ChainlinkDAppControl_createNewChainlinkAtlasWrapper() public { - MockBadChainlinkFeed mockBadFeed = new MockBadChainlinkFeed(); - - // Should revert is base feed returns price of 0 - vm.expectRevert(ChainlinkDAppControl.InvalidBaseFeed.selector); - chainlinkDAppControl.createNewChainlinkAtlasWrapper(address(mockBadFeed)); - - address predictedWrapperAddr = vm.computeCreateAddress(address(chainlinkDAppControl), vm.getNonce(address(chainlinkDAppControl))); - - vm.prank(aaveGovEOA); - vm.expectEmit(true, true, false, true); - emit ChainlinkDAppControl.NewChainlinkWrapperCreated(predictedWrapperAddr, chainlinkETHUSD, aaveGovEOA); - address wrapperAddr = chainlinkDAppControl.createNewChainlinkAtlasWrapper(chainlinkETHUSD); - - assertEq(predictedWrapperAddr, wrapperAddr, "wrapper addr not as predicted"); - assertEq(ChainlinkAtlasWrapper(payable(wrapperAddr)).owner(), aaveGovEOA, "caller is not wrapper owner"); - } - - // View Functions - - function test_ChainlinkDAppControl_getBidFormat() public view { - UserOperation memory userOp; - assertEq(chainlinkDAppControl.getBidFormat(userOp), address(0), "Bid format should be addr 0 for ETH"); - } - - function test_ChainlinkDAppControl_getBidValue() public view { - SolverOperation memory solverOp; - solverOp.bidAmount = 123; - assertEq(chainlinkDAppControl.getBidValue(solverOp), 123, "Bid value should return solverOp.bidAmount"); - } - - function test_ChainlinkDAppControl_getSignersForBaseFeed() public view { - address[] memory signersFromDAppControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - address[] memory signers = getETHUSDSigners(); - assertEq(signersFromDAppControl.length, signers.length, "Signers length should be same as expected"); - for (uint i = 0; i < signers.length; i++) { - assertEq(signersFromDAppControl[i], signers[i], "Signer should be same as expected"); - } - } - - function test_ChainlinkDAppControl_getOracleDataForBaseFeed() public view { - address[] memory signers = getETHUSDSigners(); - for (uint i = 0; i < signers.length; i++) { - Oracle memory oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, signers[i]); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - assertEq(oracle.index, i, "Oracle index not as expected"); - } - } - - function test_logUserOpTransmitData_Sepolia() public view { - console.log("Sepolia Chainlink Transmit Data in userOp.data form:"); - console.log("Sets ETH/USD Price to $2979.36"); - console.log("\n"); - console.logBytes(getTransmitPayload_Sepolia()); - } - - // ---------------------------------------------------- // - // OEV Test Utils // - // ---------------------------------------------------- // - - // Returns calldata taken from a real Chainlink ETH/USD transmit tx - function getTransmitPayload() public pure returns ( - bytes memory report, - bytes32[] memory rs, - bytes32[] memory ss, - bytes32 rawVs - ) { - // From this ETHUSD transmit tx on mainnet: - // https://etherscan.io/tx/0x3645d1bc223efe0861e02aeb95d6204c5ebfe268b64a7d23d385520faf452bc0 - // ETHUSD set to: $2941.02 == 294102000000 - // Block: 19289830 - - report = hex"000000000000000000000047ddec946856fa8055ac2202f633de330001769d050a1718161a110212090c1d1b191c0b0e030001140f131e1508060d04100705000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001f00000000000000000000000000000000000000000000000000000044759deacc0000000000000000000000000000000000000000000000000000004475d8ca020000000000000000000000000000000000000000000000000000004475d8ca020000000000000000000000000000000000000000000000000000004475d8ca100000000000000000000000000000000000000000000000000000004476517782000000000000000000000000000000000000000000000000000000447664840f0000000000000000000000000000000000000000000000000000004476a015190000000000000000000000000000000000000000000000000000004476a015190000000000000000000000000000000000000000000000000000004476a01519000000000000000000000000000000000000000000000000000000447779d953000000000000000000000000000000000000000000000000000000447914dd8f000000000000000000000000000000000000000000000000000000447914dd8f000000000000000000000000000000000000000000000000000000447914dd8f000000000000000000000000000000000000000000000000000000447914dd8f0000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d9ce27000000000000000000000000000000000000000000000000000000447a9ebec0000000000000000000000000000000000000000000000000000000447a9ebec0000000000000000000000000000000000000000000000000000000447ad9a8c8000000000000000000000000000000000000000000000000000000447b281300000000000000000000000000000000000000000000000000000000447b281300000000000000000000000000000000000000000000000000000000447b281300000000000000000000000000000000000000000000000000000000447b3df490000000000000000000000000000000000000000000000000000000447b3df490000000000000000000000000000000000000000000000000000000447b5d3856000000000000000000000000000000000000000000000000000000447b5d3856000000000000000000000000000000000000000000000000000000447b5d3856"; - rs = new bytes32[](11); - ss = new bytes32[](11); - rawVs = 0x0100010101000000000001000000000000000000000000000000000000000000; - - rs[0] = 0x5bde6c6a1b27f1421400fba9850abcd4d9701d7178d2df1d469c693a9ab6389e; - rs[1] = 0xf9518ba45aaf8fc019962dee3144087723bcac49ce1b3d12d075abc030ae51f1; - rs[2] = 0x657ae99064988794f99b98dfdd6ebc9f5c0967f75e5ce61392d81d78e8434e0a; - rs[3] = 0xfc4ef6fa4d47c3a8cb247799831c4db601387106cc9f80ef710fec5d06b07c53; - rs[4] = 0x813516330ff60244a90a062f4ddcb18611711ed217cf146041439e26a6c1d687; - rs[5] = 0x8aa23424c2fdd1d2f459fc901c3b21c611d17c07b0df51c48f17bd6bcc5d8c54; - rs[6] = 0xe40ea4755faebccfccbf9ca25bd518427877c9155526db04458b9753034ad552; - rs[7] = 0x44fbb6b9ab6f56f29d5d1943fa6d6b13c993e213ba3b20f6a5d20224cb3f942d; - rs[8] = 0xe2a4e3529c077a128bc52d5e1b11cf64bc922100bafe6ebc95654fea49a5d355; - rs[9] = 0x4588680888b56cda77a1b49b32807ba33e7009a182a048d33496d70987aebcbc; - rs[10] = 0x7ad51d2aa5e792f46ac17784a3a362f0fff3dc7f805ef8f74324113d8b475249; - - ss[0] = 0x3eb07f321322b7d0ea0c90ca48af408e9b6daaaf0e33993436155ef83d1e7d0e; - ss[1] = 0x5ff05d281bf7c1db924036e0123765adfef8b4f563d981da9c7011dc3b1e6c79; - ss[2] = 0x7fdb65f4084636a904129a327d99a8ef5cdcadc3e6e266626823eb1adab4532d; - ss[3] = 0x07025b9483f5ad5ee55f07b6cddbabc1411d9570ced7d11a3d504cf38853e8a3; - ss[4] = 0x332a4b577c831d9dae5ea3eb4ee5832cdd6672c4bd7c97e5fb2dae3b4b99d02f; - ss[5] = 0x45b4181c3b95f15fc40a40fb74344d1ef45c33dfbe99587237a1a4c875aae024; - ss[6] = 0x2a2eb5e729343c2f093c6b6ec71f0b1eb07bf133ead806976672dcbf90abcaca; - ss[7] = 0x54ca9bd4122a43f119d3d24072db5be9479f9271d32f332e76ff3a70feeb7fd3; - ss[8] = 0x1a7aaeda65f1cabb53a43f080ab8d76337107b0e7bf013096eecdefcf88af56a; - ss[9] = 0x7a1e1d7b9c865b34d966a94e21f0d2c73473a30bf03c1f8eff7b88b5c3183c31; - ss[10] = 0x2768d43bca650e9a452db41730c4e31b600f5398c49490d66f4065a0b357707f; - - return (report, rs, ss, rawVs); - } - - function getTransmitPayload_Sepolia() public pure returns ( - bytes memory userOpData - ) { - // From this ETHUSD transmit tx on sepolia: - // https://sepolia.etherscan.io/tx/0xe0ed8968e815e61bda7689721c88cb90165b147cccbe8c488e08af9e9e3feef3 - // ETHUSD set to: $2979.36 == 297935851250 - // Block: 5810243 - - bytes memory report = hex"0000000000000000000000ad329f62e1afb817fb5d7bd61a458fba00037a3503030002010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000455b1e0ad0000000000000000000000000000000000000000000000000000000455e5c4ef2000000000000000000000000000000000000000000000000000000455e5c4ef2000000000000000000000000000000000000000000000000000000455e901d52"; - - bytes32[] memory rs = new bytes32[](2); - rs[0] = 0x2ebc59983c9db753946c3ae099195c497dabfe5b5211605ed6ef8e6618113127; - rs[1] = 0x39f66421121c8562f6de434b4a245fbb61f7c6a885b438d3ec4d6e94f9838ee6; - - bytes32[] memory ss = new bytes32[](2); - ss[0] = 0x61478abeaf01232ce23a9251acff3d096bc9616c5979e59db2c68c27f4bead5e; - ss[1] = 0x5b665f3a548debf55a275f7bc78e35f5db15aafb55cad6effc83d917d24c4b54; - - bytes32 rawVs = 0x0101000000000000000000000000000000000000000000000000000000000000; - - userOpData = abi.encodeCall(ChainlinkAtlasWrapper.transmit, (report, rs, ss, rawVs)); - } - - function getETHUSDSigners() public pure returns (address[] memory) { - address[] memory signers = new address[](31); - signers[0] = 0xCdEf689d3098A796F840A26f383CE19F4f023B5B; - signers[1] = 0xb7bEA3A5d410F7c4eC2aa446ae4236F6Eed6b16A; - signers[2] = 0x5ba2D2B875626901fed647411AD08009b1ee35e2; - signers[3] = 0x21389cBcdb25c8859c704BD8Cd7252902384FceF; - signers[4] = 0x03A67cD8467642a03d5ebd67F97157931D94fA32; - signers[5] = 0x3650Da40Fe97A93bfC2623E0DcA3899a91Eca0e2; - signers[6] = 0x1F31c45AE0605690D63D26d7CdA4035c3668D473; - signers[7] = 0xCc1b49B86F79C7E50E294D3e3734fe94DB9A42F0; - signers[8] = 0xA4EBE1e06dd0bf674B0757cd20b97Ee16b00aF1B; - signers[9] = 0x8d4AE8b06701f53f7a34421461441E4492E1C578; - signers[10] = 0x5007b477F939646b4E4416AFcEf6b00567F5F078; - signers[11] = 0x55048BC9f3a3f373031fB32C0D0d5C1Bc6E10B3b; - signers[12] = 0x8316e3Eb7eccfCAF0c1967903CcA8ECda3dF37E0; - signers[13] = 0x503bd542a29F089319855cd9F6F6F937C7Be87c7; - signers[14] = 0xbd34530f411130fd7aCB88b8251009A0829379aA; - signers[15] = 0x54103390874885e164d69bf08B6db480E5E8fE5d; - signers[16] = 0xC01465eBA4FA3A72309374bb67149A8FD14Cb687; - signers[17] = 0xAF447dA1E8c277C41983B1732BECF39129BE5CA6; - signers[18] = 0xbe15B23E9F03e3Bb44c5E35549354649fb25b87B; - signers[19] = 0x1E15545b23B831fD39e1d9579427DeA61425DD47; - signers[20] = 0xD1C8b1e58C1186597D1897054b738c551ec74BD4; - signers[21] = 0x8EB664cD767f12507E7e3864Ba5B7E925090A0E5; - signers[22] = 0x656Fc633eb33cF5daD0bCEa0E42cde85fb7A4Ab8; - signers[23] = 0x076b12D219a32613cd370eA9649a860114D3015e; - signers[24] = 0x0ac6c28B582016A55f6d4e3aC77b64749568Ffe1; - signers[25] = 0xD3b9610534994aAb2777D8Af6C41d1e54F2ef33f; - signers[26] = 0xbeB19b5EC84DdC9426d84e5cE7403AFB7BB56700; - signers[27] = 0xd54DDB3A256a40061C41Eb6ADF4f412ca8e17c25; - signers[28] = 0xdb69C372B30D7A663BDE45d31a4886385F50Ea51; - signers[29] = 0x67a95e050d2E4200808A488628d55269dDeFC455; - signers[30] = 0x080D263FAA8CBd848f0b9B24B40e1f23EA06b3A3; - return signers; - } -} - -contract LiquidationOEVSolver is SolverBase { - error NotSolverOwner(); - constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { } - - function liquidate(address liquidatable) public onlySelf { - if(MockLiquidatable(liquidatable).canLiquidate()) { - MockLiquidatable(liquidatable).liquidate(); - } else { - console.log("Solver NOT able to liquidate."); - } - } - - function withdrawETH() public { - if(msg.sender != _owner) revert NotSolverOwner(); - (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); - require(success, "Withdraw failed"); - } - - // This ensures a function can only be called through atlasSolverCall - // which includes security checks to work safely with Atlas - modifier onlySelf() { - require(msg.sender == address(this), "Not called via atlasSolverCall"); - _; - } - - fallback() external payable { } - receive() external payable { } -} - -// Super basic mock to represent a liquidation payout dependent on oracle price -contract MockLiquidatable { - address public oracle; - uint256 public liquidationPrice; - - constructor(address _oracle, uint256 _liquidationPrice) { - oracle = _oracle; - liquidationPrice = _liquidationPrice; - } - - function liquidate() public { - require(canLiquidate(), "Cannot liquidate"); - require(address(this).balance > 0, "No liquidation reward available"); - // If liquidated successfully, sends all the ETH in this contract to caller - (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); - require(success, "Liquidation failed"); - } - - // Can only liquidate if the oracle price is exactly the liquidation price - function canLiquidate() public view returns (bool) { - return uint256(AggregatorV2V3Interface(oracle).latestAnswer()) == liquidationPrice; - } -} - -contract MockBadChainlinkFeed { - function latestAnswer() external pure returns(int256) { - return 0; - } -} \ No newline at end of file diff --git a/test/OEValt.t.sol b/test/OEValt.t.sol deleted file mode 100644 index 65f418191..000000000 --- a/test/OEValt.t.sol +++ /dev/null @@ -1,646 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import "forge-std/Test.sol"; - -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; - -import { BaseTest } from "test/base/BaseTest.t.sol"; -import { TxBuilder } from "src/contracts/helpers/TxBuilder.sol"; -import { UserOperationBuilder } from "test/base/builders/UserOperationBuilder.sol"; - -import { SolverOperation } from "src/contracts/types/SolverOperation.sol"; -import { UserOperation } from "src/contracts/types/UserOperation.sol"; -import { DAppConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/DAppOperation.sol"; - -import { ChainlinkDAppControl, Oracle, Role } from "src/contracts/examples/oev-example/ChainlinkDAppControlAlt.sol"; -import {ChainlinkAtlasWrapper, IChainlinkFeed } from "src/contracts/examples/oev-example/ChainlinkAtlasWrapperAlt.sol"; -import { SolverBase } from "src/contracts/solver/SolverBase.sol"; - - -// Using this Chainlink update to ETHUSD feed as an example: -// Aggregator: https://etherscan.io/address/0xE62B71cf983019BFf55bC83B48601ce8419650CC -// Transmit tx: https://etherscan.io/tx/0x3645d1bc223efe0861e02aeb95d6204c5ebfe268b64a7d23d385520faf452bc0 -// ETH/USD set to: $2941.02 == 294102000000 - -contract OEVTest is BaseTest { - ChainlinkAtlasWrapper public chainlinkAtlasWrapper; - ChainlinkDAppControl public chainlinkDAppControl; - MockLiquidatable public mockLiquidatable; - TxBuilder public txBuilder; - Sig public sig; - - address chainlinkGovEOA; - address aaveGovEOA; - address executionEnvironment; - address transmitter; - - address chainlinkETHUSD = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; - uint256 forkBlock = 19289829; // Block just before the transmit tx above - uint256 targetOracleAnswer = 294102000000; - uint256 liquidationReward = 10e18; - uint256 solverWinningBid = 1e18; - - struct TransmitData { - bytes report; - bytes32[] rs; - bytes32[] ss; - bytes32 rawVs; - } - - function setUp() public virtual override { - BaseTest.setUp(); - vm.rollFork(forkBlock); - vm.deal(solverOneEOA, 100e18); - - // Creating new gov address (SignatoryActive error if already registered with control) - uint256 chainlinkGovPK = 11_112; - uint256 aaveGovPK = 11_113; - chainlinkGovEOA = vm.addr(chainlinkGovPK); - aaveGovEOA = vm.addr(aaveGovPK); - - vm.startPrank(chainlinkGovEOA); - // Chainlink's Gov address deploys the Chainlink DAppControl - chainlinkDAppControl = new ChainlinkDAppControl(address(atlas)); - // Chainlink's Gov address initializes the Chainlink DAppControl in Atlas - atlasVerification.initializeGovernance(address(chainlinkDAppControl)); - // Set Chainlink's ETHUSD feed signers in DAppControl for verification - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, getETHUSDSigners()); - vm.stopPrank(); - - vm.startPrank(aaveGovEOA); - // Aave creates a Chainlink Atlas Wrapper for ETH/USD to capture OEV - chainlinkAtlasWrapper = ChainlinkAtlasWrapper(payable(chainlinkDAppControl.createNewChainlinkAtlasWrapper(chainlinkETHUSD))); - // OEV-generating protocols must use the Chainlink Atlas Wrapper for price feed in order to capture the OEV - mockLiquidatable = new MockLiquidatable(address(chainlinkAtlasWrapper), targetOracleAnswer); - // Aave sets the Chainlink Execution Environment as a trusted transmitter in the Chainlink Atlas Wrapper - // chainlinkAtlasWrapper.setTransmitterStatus(executionEnvironment, true); - vm.stopPrank(); - - deal(address(mockLiquidatable), liquidationReward); // Add 10 ETH as liquidation reward - - - // Set the transmitter - transmitter = chainlinkAtlasWrapper.transmitters()[3]; - - vm.startPrank(transmitter); // User is a Chainlink Node - executionEnvironment = atlas.createExecutionEnvironment(transmitter, address(chainlinkDAppControl)); - vm.stopPrank(); - - txBuilder = new TxBuilder({ - _control: address(chainlinkDAppControl), - _atlas: address(atlas), - _verification: address(atlasVerification) - }); - - vm.label(chainlinkGovEOA, "Chainlink Gov"); - vm.label(aaveGovEOA, "Aave Gov"); - vm.label(address(executionEnvironment), "EXECUTION ENV"); - vm.label(address(chainlinkAtlasWrapper), "Chainlink Atlas Wrapper"); - vm.label(address(chainlinkDAppControl), "Chainlink DApp Control"); - vm.label(address(chainlinkETHUSD), "Chainlink Base ETH/USD Feed"); - } - - // ---------------------------------------------------- // - // Full OEV Capture Test // - // ---------------------------------------------------- // - - function testChainlinkOEV_AltVersion_SkipCoverage() public { - UserOperation memory userOp; - SolverOperation[] memory solverOps = new SolverOperation[](1); - DAppOperation memory dAppOp; - - vm.startPrank(solverOneEOA); - LiquidationOEVSolver liquidationSolver = new LiquidationOEVSolver(WETH_ADDRESS, address(atlas)); - atlas.deposit{ value: 1e18 }(); - atlas.bond(1e18); - vm.stopPrank(); - - (bytes memory report, bytes32[] memory rs, bytes32[] memory ss, bytes32 rawVs) - = getTransmitPayload(); - - // Basic userOp created but excludes oracle price update data - userOp = txBuilder.buildUserOperation({ - from: transmitter, - to: address(chainlinkAtlasWrapper), // Aave's ChainlinkAtlasWrapper for ETHUSD - maxFeePerGas: tx.gasprice + 1, - value: 0, - deadline: block.number + 2, - data: "" // No userOp.data yet - only created after solverOps are signed - }); - userOp.sessionKey = governanceEOA; - - bytes memory solverOpData = - abi.encodeWithSelector(LiquidationOEVSolver.liquidate.selector, address(mockLiquidatable)); - - solverOps[0] = txBuilder.buildSolverOperation({ - userOp: userOp, - solverOpData: solverOpData, - solver: solverOneEOA, - solverContract: address(liquidationSolver), - bidAmount: solverWinningBid, - value: 0 - }); - - (sig.v, sig.r, sig.s) = vm.sign(solverOnePK, atlasVerification.getSolverPayload(solverOps[0])); - solverOps[0].signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // After solvers have signed their ops, Chainlink creates the userOp with price update data - userOp.data = abi.encodeWithSelector(ChainlinkAtlasWrapper.transmit.selector, report, rs, ss, rawVs); - - dAppOp = txBuilder.buildDAppOperation(governanceEOA, userOp, solverOps); - (sig.v, sig.r, sig.s) = vm.sign(governancePK, atlasVerification.getDAppOperationPayload(dAppOp)); - dAppOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - assertEq(mockLiquidatable.canLiquidate(), false); - assertTrue(uint(chainlinkAtlasWrapper.latestAnswer()) != targetOracleAnswer, "Wrapper answer should not be target yet"); - assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), uint(IChainlinkFeed(chainlinkETHUSD).latestAnswer()), "Wrapper and base feed should report same answer"); - assertEq(address(chainlinkAtlasWrapper).balance, 0, "Wrapper should not have any ETH"); - - // To show the signer verification checks cause metacall to pass/fail: - // uint256 snapshot = vm.snapshot(); - - // Should Succeed - vm.prank(transmitter); - atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp }); - - assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), targetOracleAnswer, "Wrapper did not update as expected"); - assertTrue(uint(chainlinkAtlasWrapper.latestAnswer()) != uint(IChainlinkFeed(chainlinkETHUSD).latestAnswer()), "Wrapper and base feed should report different answers"); - assertEq(address(chainlinkAtlasWrapper).balance, solverWinningBid, "Wrapper should hold winning bid as OEV"); - } - - // ---------------------------------------------------- // - // ChainlinkAtlasWrapper Tests // - // ---------------------------------------------------- // - - function testChainlinkAtlasWrapperViewFunctions_AltVersion_SkipCoverage() public { - // Check wrapper and base start as expected - assertEq(chainlinkAtlasWrapper.atlasLatestAnswer(), 0, "Wrapper stored latestAnswer should be 0"); - assertTrue(IChainlinkFeed(chainlinkETHUSD).latestAnswer() != 0, "Base latestAnswer should not be 0"); - assertEq(chainlinkAtlasWrapper.atlasLatestTimestamp(), 0, "Wrapper stored latestTimestamp should be 0"); - assertTrue(IChainlinkFeed(chainlinkETHUSD).latestTimestamp() != 0, "Base latestTimestamp should not be 0"); - - (uint80 roundIdAtlas, int256 answerAtlas, uint256 startedAtAtlas, uint256 updatedAtAtlas, uint80 answeredInRoundAtlas) = chainlinkAtlasWrapper.latestRoundData(); - (uint80 roundIdBase, int256 answerBase, uint256 startedAtBase, uint256 updatedAtBase, uint80 answeredInRoundBase) = IChainlinkFeed(chainlinkETHUSD).latestRoundData(); - - // Before Atlas price update, all view functions should fall back to base oracle - assertEq(chainlinkAtlasWrapper.latestAnswer(), IChainlinkFeed(chainlinkETHUSD).latestAnswer(), "latestAnswer should be same as base"); - assertEq(chainlinkAtlasWrapper.latestTimestamp(), IChainlinkFeed(chainlinkETHUSD).latestTimestamp(), "latestTimestamp should be same as base"); - assertEq(roundIdAtlas, roundIdBase, "roundId should be same as base"); - assertEq(answerAtlas, answerBase, "answer should be same as base"); - assertEq(startedAtAtlas, startedAtBase, "startedAt should be same as base"); - assertEq(updatedAtAtlas, updatedAtBase, "updatedAt should be same as base"); - assertEq(answeredInRoundAtlas, answeredInRoundBase, "answeredInRound should be same as base"); - - // Update wrapper with new price by calling transmit from an approved EE - TransmitData memory transmitData; - (transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs) = getTransmitPayload(); - vm.prank(executionEnvironment); - chainlinkAtlasWrapper.transmit(transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs); - - // After Atlas price update, latestAnswer and latestTimestamp should be different to base oracle - assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), targetOracleAnswer, "latestAnswer should be updated"); - assertTrue(uint(chainlinkAtlasWrapper.latestAnswer()) != uint(IChainlinkFeed(chainlinkETHUSD).latestAnswer()), "latestAnswer should be different to base"); - assertEq(chainlinkAtlasWrapper.latestTimestamp(), block.timestamp, "latestTimestamp should be updated"); - assertTrue(chainlinkAtlasWrapper.latestTimestamp() > IChainlinkFeed(chainlinkETHUSD).latestTimestamp(), "latestTimestamp should be later than base"); - - (roundIdAtlas, answerAtlas, startedAtAtlas, updatedAtAtlas, answeredInRoundAtlas) = chainlinkAtlasWrapper.latestRoundData(); - (roundIdBase, answerBase, startedAtBase, updatedAtBase, answeredInRoundBase) = IChainlinkFeed(chainlinkETHUSD).latestRoundData(); - - assertEq(roundIdAtlas, roundIdBase, "roundId should still be same as base"); - assertTrue(answerAtlas == int(targetOracleAnswer) && answerAtlas != answerBase, "answer should be updated"); - assertEq(startedAtAtlas, startedAtBase, "startedAt should still be same as base"); - assertTrue(updatedAtAtlas > updatedAtBase, "updatedAt should be later than base"); - assertEq(answeredInRoundAtlas, answeredInRoundBase, "answeredInRound should still be same as base"); - } - - function testChainlinkAtlasWrapperWithdrawFunctions_AltVersion() public { - uint256 startETH = 10e18; - uint256 startDai = 5e18; - deal(address(chainlinkAtlasWrapper), startETH); // Give wrapper 10 ETH - deal(address(DAI), address(chainlinkAtlasWrapper), startDai); // Give wrapper 5 DAI - - assertEq(address(chainlinkAtlasWrapper).balance, startETH, "Wrapper should have 10 ETH"); - assertEq(DAI.balanceOf(address(chainlinkAtlasWrapper)), startDai, "Wrapper should have 5 DAI"); - assertEq(aaveGovEOA.balance, 0, "Aave Gov should have 0 ETH"); - assertEq(DAI.balanceOf(aaveGovEOA), 0, "Aave Gov should have 0 DAI"); - - vm.startPrank(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, chainlinkGovEOA)); - chainlinkAtlasWrapper.withdrawETH(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, chainlinkGovEOA)); - chainlinkAtlasWrapper.withdrawERC20(address(DAI), chainlinkGovEOA); - vm.stopPrank(); - - assertEq(aaveGovEOA.balance, 0, "Aave Gov should still have 0 ETH"); - assertEq(DAI.balanceOf(aaveGovEOA), 0, "Aave Gov should still have 0 DAI"); - - vm.startPrank(aaveGovEOA); - chainlinkAtlasWrapper.withdrawETH(aaveGovEOA); - chainlinkAtlasWrapper.withdrawERC20(address(DAI), aaveGovEOA); - vm.stopPrank(); - - assertEq(address(chainlinkAtlasWrapper).balance, 0, "Wrapper should have 0 ETH"); - assertEq(DAI.balanceOf(address(chainlinkAtlasWrapper)), 0, "Wrapper should have 0 DAI"); - assertEq(aaveGovEOA.balance, startETH, "Aave Gov should have 10 ETH"); - assertEq(DAI.balanceOf(aaveGovEOA), startDai, "Aave Gov should have 5 DAI"); - } - - function testChainlinkAtlasWrapperTransmit_AltVersion_SkipCoverage() public { - TransmitData memory transmitData; - (transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs) = getTransmitPayload(); - - assertEq(chainlinkAtlasWrapper.atlasLatestAnswer(), 0, "Wrapper stored latestAnswer should be 0"); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(); - chainlinkAtlasWrapper.transmit(transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs); - - assertEq(chainlinkAtlasWrapper.atlasLatestAnswer(), 0, "Wrapper stored latestAnswer should still be 0"); - - vm.prank(executionEnvironment); - chainlinkAtlasWrapper.transmit(transmitData.report, transmitData.rs, transmitData.ss, transmitData.rawVs); - - assertEq(uint(chainlinkAtlasWrapper.atlasLatestAnswer()), targetOracleAnswer, "Wrapper stored latestAnswer should be updated"); - } - - function testChainlinkAtlasWrapperCanReceiveETH_AltVersion() public { - deal(transmitter, 2e18); - - assertEq(address(chainlinkAtlasWrapper).balance, 0, "Wrapper should have 0 ETH"); - - vm.startPrank(transmitter); - payable(address(chainlinkAtlasWrapper)).transfer(1e18); - (bool success, ) = address(chainlinkAtlasWrapper).call{value: 1e18}(""); - vm.stopPrank(); - - assertTrue(success, "Transfer should succeed"); - assertEq(address(chainlinkAtlasWrapper).balance, 2e18, "Wrapper should have 2 ETH"); - } - - // ---------------------------------------------------- // - // ChainlinkDAppControl Tests // - // ---------------------------------------------------- // - - function test_ChainlinkDAppControl_setSignersForBaseFeed_AltVersion() public { - address[] memory signers = getETHUSDSigners(); - address[] memory signersFromDAppControl; - - vm.expectRevert(ChainlinkDAppControl.OnlyGovernance.selector); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, signers); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignersSetForBaseFeed(chainlinkETHUSD, signers); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, signers); - - signersFromDAppControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - assertEq(signersFromDAppControl.length, signers.length, "Signers length should be same as expected"); - for (uint i = 0; i < signers.length; i++) { - assertEq(signersFromDAppControl[i], signers[i], "Signer should be same as expected"); - } - - address[] memory blankSigners = new address[](0); - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignersSetForBaseFeed(chainlinkETHUSD, blankSigners); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, blankSigners); - - assertEq(chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD).length, 0, "Signers should be empty"); - - // Should revert on too many signers - address[] memory tooManySigners = new address[](chainlinkDAppControl.MAX_NUM_ORACLES() + 1); - for (uint i = 0; i < signers.length; i++) { - tooManySigners[i] = signers[i]; - } - tooManySigners[chainlinkDAppControl.MAX_NUM_ORACLES()] = chainlinkGovEOA; - - vm.prank(chainlinkGovEOA); - vm.expectRevert(ChainlinkDAppControl.TooManySigners.selector); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, tooManySigners); - - // Should revert on duplicate signers in the array - address[] memory duplicateSigners = new address[](2); - duplicateSigners[0] = chainlinkGovEOA; - duplicateSigners[1] = chainlinkGovEOA; - - vm.prank(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(ChainlinkDAppControl.DuplicateSigner.selector, chainlinkGovEOA)); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, duplicateSigners); - - // Check properties set correctly on valid signer set - address[] memory validSigners = new address[](2); - validSigners[0] = chainlinkGovEOA; - validSigners[1] = aaveGovEOA; - - Oracle memory oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - assertEq(uint(oracle.role), uint(Role.Unset), "Oracle role should be Unset"); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignersSetForBaseFeed(chainlinkETHUSD, validSigners); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, validSigners); - - address[] memory signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - assertEq(signersFromDappControl.length, validSigners.length, "length mismatch"); - assertEq(signersFromDappControl[0], validSigners[0]); - assertEq(signersFromDappControl[1], validSigners[1]); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - assertEq(oracle.index, 0, "Oracle index should be 0"); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, aaveGovEOA); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - assertEq(oracle.index, 1, "Oracle index should be 1"); - } - - function test_ChainlinkDAppControl_addSignerForBaseFeed_AltVersion() public { - vm.expectRevert(ChainlinkDAppControl.OnlyGovernance.selector); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - - address[] memory signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - assertEq(signersFromDappControl.length, chainlinkDAppControl.MAX_NUM_ORACLES(), "Should have max signers"); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(ChainlinkDAppControl.TooManySigners.selector); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - - // clear signers so we can add individually - address[] memory blankSigners = new address[](0); - vm.prank(chainlinkGovEOA); - chainlinkDAppControl.setSignersForBaseFeed(chainlinkETHUSD, blankSigners); - assertEq(chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD).length, 0, "Signers should be empty"); - - Oracle memory oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - assertEq(uint(oracle.role), uint(Role.Unset), "Oracle role should be Unset"); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignerAddedForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - - signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - assertEq(signersFromDappControl.length, 1, "Signers should have 1"); - assertEq(signersFromDappControl[0], chainlinkGovEOA, "Signer should be chainlinkGovEOA"); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(abi.encodeWithSelector(ChainlinkDAppControl.DuplicateSigner.selector, chainlinkGovEOA)); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, chainlinkGovEOA); - - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, address(1)); - assertEq(uint(oracle.role), uint(Role.Unset), "Oracle role should be Unset"); - assertEq(oracle.index, 0, "Oracle index should be 0"); - - vm.prank(chainlinkGovEOA); - chainlinkDAppControl.addSignerForBaseFeed(chainlinkETHUSD, address(1)); - - signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, address(1)); - assertEq(signersFromDappControl.length, 2, "Signers should have 2"); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - assertEq(oracle.index, 1, "Oracle index should be 1"); - } - - function test_ChainlinkDAppControl_removeSignerOfBaseFeed_AltVersion() public { - address[] memory realSigners = getETHUSDSigners(); - address signerToRemove = realSigners[10]; - address[] memory signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - Oracle memory oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, signerToRemove); - assertEq(signersFromDappControl.length, 31, "Should have 31 signers at start"); - assertEq(signersFromDappControl[10], realSigners[10], "targeted signer still registered"); - assertEq(oracle.index, 10); - assertEq(uint(oracle.role), uint(Role.Signer)); - - vm.expectRevert(ChainlinkDAppControl.OnlyGovernance.selector); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, realSigners[30]); - - vm.prank(chainlinkGovEOA); - vm.expectRevert(ChainlinkDAppControl.SignerNotFound.selector); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, address(1)); - - vm.prank(chainlinkGovEOA); - vm.expectEmit(true, false, false, true); - emit ChainlinkDAppControl.SignerRemovedForBaseFeed(chainlinkETHUSD, signerToRemove); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, signerToRemove); - - signersFromDappControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, signerToRemove); - assertEq(signersFromDappControl.length, realSigners.length - 1, "Should have 30 signers now"); - assertTrue(signersFromDappControl[10] != realSigners[10], "Idx 10 signers should be diff now"); - assertEq(oracle.index, 0); - assertEq(uint(oracle.role), uint(Role.Unset)); - } - - function test_ChainlinkDAppControl_verifyTransmitSigners_AltVersion() public { - // 3rd signer in the ETHUSD transmit example tx used - address signerToRemove = 0xCc1b49B86F79C7E50E294D3e3734fe94DB9A42F0; - ( - bytes memory report, bytes32[] memory rs, - bytes32[] memory ss, bytes32 rawVs - ) = getTransmitPayload(); - - // All signers should be verified - assertEq(chainlinkDAppControl.verifyTransmitSigners(chainlinkETHUSD, report, rs, ss, rawVs), true); - - // If a verified signer is removed from DAppControl, should return false - vm.prank(chainlinkGovEOA); - chainlinkDAppControl.removeSignerOfBaseFeed(chainlinkETHUSD, signerToRemove); - assertEq(chainlinkDAppControl.verifyTransmitSigners(chainlinkETHUSD, report, rs, ss, rawVs), false); - } - - function test_ChainlinkDAppControl_createNewChainlinkAtlasWrapper_AltVersion() public { - MockBadChainlinkFeed mockBadFeed = new MockBadChainlinkFeed(); - - // Should revert is base feed returns price of 0 - vm.expectRevert(ChainlinkDAppControl.InvalidBaseFeed.selector); - chainlinkDAppControl.createNewChainlinkAtlasWrapper(address(mockBadFeed)); - - address predictedWrapperAddr = vm.computeCreateAddress(address(chainlinkDAppControl), vm.getNonce(address(chainlinkDAppControl))); - - vm.prank(aaveGovEOA); - vm.expectEmit(true, true, false, true); - emit ChainlinkDAppControl.NewChainlinkWrapperCreated(predictedWrapperAddr, chainlinkETHUSD, aaveGovEOA); - address wrapperAddr = chainlinkDAppControl.createNewChainlinkAtlasWrapper(chainlinkETHUSD); - - assertEq(predictedWrapperAddr, wrapperAddr, "wrapper addr not as predicted"); - assertEq(ChainlinkAtlasWrapper(payable(wrapperAddr)).owner(), aaveGovEOA, "caller is not wrapper owner"); - } - - // View Functions - - function test_ChainlinkDAppControl_getBidFormat_AltVersion() public view { - UserOperation memory userOp; - assertEq(chainlinkDAppControl.getBidFormat(userOp), address(0), "Bid format should be addr 0 for ETH"); - } - - function test_ChainlinkDAppControl_getBidValue_AltVersion() public view { - SolverOperation memory solverOp; - solverOp.bidAmount = 123; - assertEq(chainlinkDAppControl.getBidValue(solverOp), 123, "Bid value should return solverOp.bidAmount"); - } - - function test_ChainlinkDAppControl_getSignersForBaseFeed_AltVersion() public view { - address[] memory signersFromDAppControl = chainlinkDAppControl.getSignersForBaseFeed(chainlinkETHUSD); - address[] memory signers = getETHUSDSigners(); - assertEq(signersFromDAppControl.length, signers.length, "Signers length should be same as expected"); - for (uint i = 0; i < signers.length; i++) { - assertEq(signersFromDAppControl[i], signers[i], "Signer should be same as expected"); - } - } - - function test_ChainlinkDAppControl_getOracleDataForBaseFeed_AltVersion() public view { - address[] memory signers = getETHUSDSigners(); - for (uint i = 0; i < signers.length; i++) { - Oracle memory oracle = chainlinkDAppControl.getOracleDataForBaseFeed(chainlinkETHUSD, signers[i]); - assertEq(uint(oracle.role), uint(Role.Signer), "Oracle role should be Signer"); - assertEq(oracle.index, i, "Oracle index not as expected"); - } - } - - - // ---------------------------------------------------- // - // OEV Test Utils // - // ---------------------------------------------------- // - - // Returns calldata taken from a real Chainlink ETH/USD transmit tx - function getTransmitPayload() public pure returns ( - bytes memory report, - bytes32[] memory rs, - bytes32[] memory ss, - bytes32 rawVs - ) { - // From this ETHUSD transmit tx: Transmit tx: - // https://etherscan.io/tx/0x3645d1bc223efe0861e02aeb95d6204c5ebfe268b64a7d23d385520faf452bc0 - // ETHUSD set to: $2941.02 == 294102000000 - // Block: 19289830 - - report = hex"000000000000000000000047ddec946856fa8055ac2202f633de330001769d050a1718161a110212090c1d1b191c0b0e030001140f131e1508060d04100705000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001f00000000000000000000000000000000000000000000000000000044759deacc0000000000000000000000000000000000000000000000000000004475d8ca020000000000000000000000000000000000000000000000000000004475d8ca020000000000000000000000000000000000000000000000000000004475d8ca100000000000000000000000000000000000000000000000000000004476517782000000000000000000000000000000000000000000000000000000447664840f0000000000000000000000000000000000000000000000000000004476a015190000000000000000000000000000000000000000000000000000004476a015190000000000000000000000000000000000000000000000000000004476a01519000000000000000000000000000000000000000000000000000000447779d953000000000000000000000000000000000000000000000000000000447914dd8f000000000000000000000000000000000000000000000000000000447914dd8f000000000000000000000000000000000000000000000000000000447914dd8f000000000000000000000000000000000000000000000000000000447914dd8f0000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d861800000000000000000000000000000000000000000000000000000004479d9ce27000000000000000000000000000000000000000000000000000000447a9ebec0000000000000000000000000000000000000000000000000000000447a9ebec0000000000000000000000000000000000000000000000000000000447ad9a8c8000000000000000000000000000000000000000000000000000000447b281300000000000000000000000000000000000000000000000000000000447b281300000000000000000000000000000000000000000000000000000000447b281300000000000000000000000000000000000000000000000000000000447b3df490000000000000000000000000000000000000000000000000000000447b3df490000000000000000000000000000000000000000000000000000000447b5d3856000000000000000000000000000000000000000000000000000000447b5d3856000000000000000000000000000000000000000000000000000000447b5d3856"; - rs = new bytes32[](11); - ss = new bytes32[](11); - rawVs = 0x0100010101000000000001000000000000000000000000000000000000000000; - - rs[0] = 0x5bde6c6a1b27f1421400fba9850abcd4d9701d7178d2df1d469c693a9ab6389e; - rs[1] = 0xf9518ba45aaf8fc019962dee3144087723bcac49ce1b3d12d075abc030ae51f1; - rs[2] = 0x657ae99064988794f99b98dfdd6ebc9f5c0967f75e5ce61392d81d78e8434e0a; - rs[3] = 0xfc4ef6fa4d47c3a8cb247799831c4db601387106cc9f80ef710fec5d06b07c53; - rs[4] = 0x813516330ff60244a90a062f4ddcb18611711ed217cf146041439e26a6c1d687; - rs[5] = 0x8aa23424c2fdd1d2f459fc901c3b21c611d17c07b0df51c48f17bd6bcc5d8c54; - rs[6] = 0xe40ea4755faebccfccbf9ca25bd518427877c9155526db04458b9753034ad552; - rs[7] = 0x44fbb6b9ab6f56f29d5d1943fa6d6b13c993e213ba3b20f6a5d20224cb3f942d; - rs[8] = 0xe2a4e3529c077a128bc52d5e1b11cf64bc922100bafe6ebc95654fea49a5d355; - rs[9] = 0x4588680888b56cda77a1b49b32807ba33e7009a182a048d33496d70987aebcbc; - rs[10] = 0x7ad51d2aa5e792f46ac17784a3a362f0fff3dc7f805ef8f74324113d8b475249; - - ss[0] = 0x3eb07f321322b7d0ea0c90ca48af408e9b6daaaf0e33993436155ef83d1e7d0e; - ss[1] = 0x5ff05d281bf7c1db924036e0123765adfef8b4f563d981da9c7011dc3b1e6c79; - ss[2] = 0x7fdb65f4084636a904129a327d99a8ef5cdcadc3e6e266626823eb1adab4532d; - ss[3] = 0x07025b9483f5ad5ee55f07b6cddbabc1411d9570ced7d11a3d504cf38853e8a3; - ss[4] = 0x332a4b577c831d9dae5ea3eb4ee5832cdd6672c4bd7c97e5fb2dae3b4b99d02f; - ss[5] = 0x45b4181c3b95f15fc40a40fb74344d1ef45c33dfbe99587237a1a4c875aae024; - ss[6] = 0x2a2eb5e729343c2f093c6b6ec71f0b1eb07bf133ead806976672dcbf90abcaca; - ss[7] = 0x54ca9bd4122a43f119d3d24072db5be9479f9271d32f332e76ff3a70feeb7fd3; - ss[8] = 0x1a7aaeda65f1cabb53a43f080ab8d76337107b0e7bf013096eecdefcf88af56a; - ss[9] = 0x7a1e1d7b9c865b34d966a94e21f0d2c73473a30bf03c1f8eff7b88b5c3183c31; - ss[10] = 0x2768d43bca650e9a452db41730c4e31b600f5398c49490d66f4065a0b357707f; - - return (report, rs, ss, rawVs); - } - - function getETHUSDSigners() public pure returns (address[] memory) { - address[] memory signers = new address[](31); - signers[0] = 0xCdEf689d3098A796F840A26f383CE19F4f023B5B; - signers[1] = 0xb7bEA3A5d410F7c4eC2aa446ae4236F6Eed6b16A; - signers[2] = 0x5ba2D2B875626901fed647411AD08009b1ee35e2; - signers[3] = 0x21389cBcdb25c8859c704BD8Cd7252902384FceF; - signers[4] = 0x03A67cD8467642a03d5ebd67F97157931D94fA32; - signers[5] = 0x3650Da40Fe97A93bfC2623E0DcA3899a91Eca0e2; - signers[6] = 0x1F31c45AE0605690D63D26d7CdA4035c3668D473; - signers[7] = 0xCc1b49B86F79C7E50E294D3e3734fe94DB9A42F0; - signers[8] = 0xA4EBE1e06dd0bf674B0757cd20b97Ee16b00aF1B; - signers[9] = 0x8d4AE8b06701f53f7a34421461441E4492E1C578; - signers[10] = 0x5007b477F939646b4E4416AFcEf6b00567F5F078; - signers[11] = 0x55048BC9f3a3f373031fB32C0D0d5C1Bc6E10B3b; - signers[12] = 0x8316e3Eb7eccfCAF0c1967903CcA8ECda3dF37E0; - signers[13] = 0x503bd542a29F089319855cd9F6F6F937C7Be87c7; - signers[14] = 0xbd34530f411130fd7aCB88b8251009A0829379aA; - signers[15] = 0x54103390874885e164d69bf08B6db480E5E8fE5d; - signers[16] = 0xC01465eBA4FA3A72309374bb67149A8FD14Cb687; - signers[17] = 0xAF447dA1E8c277C41983B1732BECF39129BE5CA6; - signers[18] = 0xbe15B23E9F03e3Bb44c5E35549354649fb25b87B; - signers[19] = 0x1E15545b23B831fD39e1d9579427DeA61425DD47; - signers[20] = 0xD1C8b1e58C1186597D1897054b738c551ec74BD4; - signers[21] = 0x8EB664cD767f12507E7e3864Ba5B7E925090A0E5; - signers[22] = 0x656Fc633eb33cF5daD0bCEa0E42cde85fb7A4Ab8; - signers[23] = 0x076b12D219a32613cd370eA9649a860114D3015e; - signers[24] = 0x0ac6c28B582016A55f6d4e3aC77b64749568Ffe1; - signers[25] = 0xD3b9610534994aAb2777D8Af6C41d1e54F2ef33f; - signers[26] = 0xbeB19b5EC84DdC9426d84e5cE7403AFB7BB56700; - signers[27] = 0xd54DDB3A256a40061C41Eb6ADF4f412ca8e17c25; - signers[28] = 0xdb69C372B30D7A663BDE45d31a4886385F50Ea51; - signers[29] = 0x67a95e050d2E4200808A488628d55269dDeFC455; - signers[30] = 0x080D263FAA8CBd848f0b9B24B40e1f23EA06b3A3; - return signers; - } -} - -contract LiquidationOEVSolver is SolverBase { - error NotSolverOwner(); - constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { } - - function liquidate(address liquidatable) public onlySelf { - if(MockLiquidatable(liquidatable).canLiquidate()) { - MockLiquidatable(liquidatable).liquidate(); - } else { - console.log("Solver NOT able to liquidate."); - } - } - - function withdrawETH() public { - if(msg.sender != _owner) revert NotSolverOwner(); - (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); - require(success, "Withdraw failed"); - } - - // This ensures a function can only be called through atlasSolverCall - // which includes security checks to work safely with Atlas - modifier onlySelf() { - require(msg.sender == address(this), "Not called via atlasSolverCall"); - _; - } - - fallback() external payable { } - receive() external payable { } -} - -// Super basic mock to represent a liquidation payout dependent on oracle price -contract MockLiquidatable { - address public oracle; - uint256 public liquidationPrice; - - constructor(address _oracle, uint256 _liquidationPrice) { - oracle = _oracle; - liquidationPrice = _liquidationPrice; - } - - function liquidate() public { - require(canLiquidate(), "Cannot liquidate"); - require(address(this).balance > 0, "No liquidation reward available"); - // If liquidated successfully, sends all the ETH in this contract to caller - (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); - require(success, "Liquidation failed"); - } - - // Can only liquidate if the oracle price is exactly the liquidation price - function canLiquidate() public view returns (bool) { - return uint256(IChainlinkFeed(oracle).latestAnswer()) == liquidationPrice; - } -} - -contract MockBadChainlinkFeed { - function latestAnswer() external pure returns(int256) { - return 0; - } -} \ No newline at end of file diff --git a/test/SwapIntent.t.sol b/test/SwapIntent.t.sol deleted file mode 100644 index 09ccef145..000000000 --- a/test/SwapIntent.t.sol +++ /dev/null @@ -1,407 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import "forge-std/Test.sol"; - -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -import { BaseTest } from "./base/BaseTest.t.sol"; -import { TxBuilder } from "src/contracts/helpers/TxBuilder.sol"; - -import { SolverOperation } from "src/contracts/types/SolverOperation.sol"; -import { UserOperation } from "src/contracts/types/UserOperation.sol"; -import { DAppConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/DAppOperation.sol"; - -import { - SwapIntentDAppControl, - SwapIntent, - Condition -} from "src/contracts/examples/intents-example/SwapIntentDAppControl.sol"; -import { SolverBase } from "src/contracts/solver/SolverBase.sol"; - -interface IUniV2Router02 { - function swapExactTokensForTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external - returns (uint256[] memory amounts); -} - -contract SwapIntentTest is BaseTest { - SwapIntentDAppControl public swapIntentControl; - TxBuilder public txBuilder; - Sig public sig; - - function setUp() public virtual override { - BaseTest.setUp(); - - // Deploy new SwapIntent Control from new gov and initialize in Atlas - vm.startPrank(governanceEOA); - swapIntentControl = new SwapIntentDAppControl(address(atlas)); - atlasVerification.initializeGovernance(address(swapIntentControl)); - vm.stopPrank(); - - txBuilder = new TxBuilder({ - _control: address(swapIntentControl), - _atlas: address(atlas), - _verification: address(atlasVerification) - }); - - deal(WETH_ADDRESS, userEOA, 10e18); - } - - function testAtlasSwapIntentWithBasicRFQ_GasCheck_SkipCoverage() public { - // Swap 10 WETH for 20 DAI - UserCondition userCondition = new UserCondition(); - - Condition[] memory conditions = new Condition[](2); - conditions[0] = Condition({ - antecedent: address(userCondition), - context: abi.encodeCall(UserCondition.isLessThanFive, 3) - }); - conditions[1] = Condition({ - antecedent: address(userCondition), - context: abi.encodeCall(UserCondition.isLessThanFive, 4) - }); - - SwapIntent memory swapIntent = SwapIntent({ - tokenUserBuys: DAI_ADDRESS, - amountUserBuys: 20e18, - tokenUserSells: WETH_ADDRESS, - amountUserSells: 10e18, - auctionBaseCurrency: address(0), - conditions: conditions - }); - - // Solver deploys the RFQ solver contract (defined at bottom of this file) - vm.startPrank(solverOneEOA); - SimpleRFQSolver rfqSolver = new SimpleRFQSolver(WETH_ADDRESS, address(atlas)); - atlas.deposit{ value: 1e18 }(); - atlas.bond(1 ether); - vm.stopPrank(); - - // Give 20 DAI to RFQ solver contract - deal(DAI_ADDRESS, address(rfqSolver), swapIntent.amountUserBuys); - assertEq(DAI.balanceOf(address(rfqSolver)), swapIntent.amountUserBuys, "Did not give enough DAI to solver"); - - // Input params for Atlas.metacall() - will be populated below - UserOperation memory userOp; - SolverOperation[] memory solverOps = new SolverOperation[](1); - DAppOperation memory dAppOp; - - vm.startPrank(userEOA); - address executionEnvironment = atlas.createExecutionEnvironment(userEOA, txBuilder.control()); - console.log("executionEnvironment", executionEnvironment); - vm.stopPrank(); - vm.label(address(executionEnvironment), "EXECUTION ENV"); - - // userOpData is used in delegatecall from exec env to control, calling preOpsCall - // first 4 bytes are "userSelector" param in preOpsCall in DAppControl - swap() selector - // rest of data is "userData" param - - // swap(SwapIntent calldata) selector = 0x98434997 - bytes memory userOpData = abi.encodeCall(SwapIntentDAppControl.swap, swapIntent); - - // Builds the metaTx and to parts of userOp, signature still to be set - userOp = txBuilder.buildUserOperation({ - from: userEOA, - to: address(swapIntentControl), - maxFeePerGas: tx.gasprice + 1, - value: 0, - deadline: block.number + 2, - data: userOpData - }); - userOp.sessionKey = governanceEOA; - - // User signs the userOp - // user doees NOT sign the userOp for when they are bundling - // (sig.v, sig.r, sig.s) = vm.sign(userPK, atlas.getUserOperationPayload(userOp)); - // userOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // Build solver calldata (function selector on solver contract and its params) - bytes memory solverOpData = - abi.encodeCall(SimpleRFQSolver.fulfillRFQ, (swapIntent, executionEnvironment)); - - // Builds the SolverOperation - solverOps[0] = txBuilder.buildSolverOperation({ - userOp: userOp, - solverOpData: solverOpData, - solver: solverOneEOA, - solverContract: address(rfqSolver), - bidAmount: 1e18, - value: 0 - }); - - // Solver signs the solverOp - (sig.v, sig.r, sig.s) = vm.sign(solverOnePK, atlasVerification.getSolverPayload(solverOps[0])); - solverOps[0].signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // Frontend creates dAppOp calldata after seeing rest of data - dAppOp = txBuilder.buildDAppOperation(governanceEOA, userOp, solverOps); - - // Frontend signs the dAppOp payload - (sig.v, sig.r, sig.s) = vm.sign(governancePK, atlasVerification.getDAppOperationPayload(dAppOp)); - dAppOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // Check user token balances before - uint256 userWethBalanceBefore = WETH.balanceOf(userEOA); - uint256 userDaiBalanceBefore = DAI.balanceOf(userEOA); - - vm.prank(userEOA); // Burn all users WETH except 10 so logs are more readable - WETH.transfer(address(1), userWethBalanceBefore - swapIntent.amountUserSells); - userWethBalanceBefore = WETH.balanceOf(userEOA); - - assertTrue(userWethBalanceBefore >= swapIntent.amountUserSells, "Not enough starting WETH"); - - console.log("\nBEFORE METACALL"); - console.log("User WETH balance", WETH.balanceOf(userEOA)); - console.log("User DAI balance", DAI.balanceOf(userEOA)); - console.log("Solver WETH balance", WETH.balanceOf(address(rfqSolver))); - console.log("Solver DAI balance", DAI.balanceOf(address(rfqSolver))); - - vm.startPrank(userEOA); - - (bool simResult,,) = simulator.simUserOperation(userOp); - assertFalse(simResult, "metasimUserOperationcall tested true a"); - - WETH.approve(address(atlas), swapIntent.amountUserSells); - - (simResult,,) = simulator.simUserOperation(userOp); - assertTrue(simResult, "metasimUserOperationcall tested false c"); - - uint256 gasLeftBefore = gasleft(); - - atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp }); - - console.log("Metacall Gas Cost:", gasLeftBefore - gasleft()); - vm.stopPrank(); - - console.log("\nAFTER METACALL"); - console.log("User WETH balance", WETH.balanceOf(userEOA)); - console.log("User DAI balance", DAI.balanceOf(userEOA)); - console.log("Solver WETH balance", WETH.balanceOf(address(rfqSolver))); - console.log("Solver DAI balance", DAI.balanceOf(address(rfqSolver))); - - // Check user token balances after - assertEq( - WETH.balanceOf(userEOA), userWethBalanceBefore - swapIntent.amountUserSells, "Did not spend enough WETH" - ); - assertEq(DAI.balanceOf(userEOA), userDaiBalanceBefore + swapIntent.amountUserBuys, "Did not receive enough DAI"); - } - - function testAtlasSwapIntentWithUniswapSolver_SkipCoverage() public { - // Swap 10 WETH for 20 DAI - Condition[] memory conditions; - - SwapIntent memory swapIntent = SwapIntent({ - tokenUserBuys: DAI_ADDRESS, - amountUserBuys: 20e18, - tokenUserSells: WETH_ADDRESS, - amountUserSells: 10e18, - auctionBaseCurrency: address(0), - conditions: conditions - }); - - // Solver deploys the RFQ solver contract (defined at bottom of this file) - vm.startPrank(solverOneEOA); - UniswapIntentSolver uniswapSolver = new UniswapIntentSolver(WETH_ADDRESS, address(atlas)); - deal(WETH_ADDRESS, address(uniswapSolver), 1e18); // 1 WETH to solver to pay bid - atlas.deposit{ value: 1e18 }(); - atlas.bond(1 ether); - vm.stopPrank(); - - // Input params for Atlas.metacall() - will be populated below - UserOperation memory userOp; - SolverOperation[] memory solverOps = new SolverOperation[](1); - DAppOperation memory dAppOp; - - vm.startPrank(userEOA); - address executionEnvironment = atlas.createExecutionEnvironment(userEOA, txBuilder.control()); - console.log("executionEnvironment a", executionEnvironment); - vm.stopPrank(); - vm.label(address(executionEnvironment), "EXECUTION ENV"); - - // userOpData is used in delegatecall from exec env to control, calling preOpsCall - // first 4 bytes are "userSelector" param in preOpsCall in DAppControl - swap() selector - // rest of data is "userData" param - - // swap(SwapIntent calldata) selector = 0x98434997 - bytes memory userOpData = abi.encodeCall(SwapIntentDAppControl.swap, swapIntent); - - // Builds the metaTx and to parts of userOp, signature still to be set - userOp = txBuilder.buildUserOperation({ - from: userEOA, - to: address(swapIntentControl), - maxFeePerGas: tx.gasprice + 1, - value: 0, - deadline: block.number + 2, - data: userOpData - }); - userOp.sessionKey = governanceEOA; - - // User signs the userOp - // user doees NOT sign the userOp when they are bundling - // (sig.v, sig.r, sig.s) = vm.sign(userPK, atlas.getUserOperationPayload(userOp)); - // userOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // Build solver calldata (function selector on solver contract and its params) - bytes memory solverOpData = - abi.encodeCall(UniswapIntentSolver.fulfillWithSwap, (swapIntent, executionEnvironment)); - - // Builds the SolverOperation - solverOps[0] = txBuilder.buildSolverOperation({ - userOp: userOp, - solverOpData: solverOpData, - solver: solverOneEOA, - solverContract: address(uniswapSolver), - bidAmount: 1e18, - value: 0 - }); - - // Solver signs the solverOp - (sig.v, sig.r, sig.s) = vm.sign(solverOnePK, atlasVerification.getSolverPayload(solverOps[0])); - solverOps[0].signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // Frontend creates dAppOp calldata after seeing rest of data - dAppOp = txBuilder.buildDAppOperation(governanceEOA, userOp, solverOps); - - // Frontend signs the dAppOp payload - (sig.v, sig.r, sig.s) = vm.sign(governancePK, atlasVerification.getDAppOperationPayload(dAppOp)); - dAppOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // Check user token balances before - uint256 userWethBalanceBefore = WETH.balanceOf(userEOA); - uint256 userDaiBalanceBefore = DAI.balanceOf(userEOA); - - vm.prank(userEOA); // Burn all users WETH except 10 so logs are more readable - WETH.transfer(address(1), userWethBalanceBefore - swapIntent.amountUserSells); - userWethBalanceBefore = WETH.balanceOf(userEOA); - - assertTrue(userWethBalanceBefore >= swapIntent.amountUserSells, "Not enough starting WETH"); - - console.log("\nBEFORE METACALL"); - console.log("User WETH balance", WETH.balanceOf(userEOA)); - console.log("User DAI balance", DAI.balanceOf(userEOA)); - console.log("Solver WETH balance", WETH.balanceOf(address(uniswapSolver))); - console.log("Solver DAI balance", DAI.balanceOf(address(uniswapSolver))); - - vm.startPrank(userEOA); - - (bool simResult,,) = simulator.simUserOperation(userOp); - assertFalse(simResult, "metasimUserOperationcall tested true a"); - - WETH.approve(address(atlas), swapIntent.amountUserSells); - - (simResult,,) = simulator.simUserOperation(userOp); - assertTrue(simResult, "metasimUserOperationcall tested false c"); - - // Check solver does NOT have DAI - it must use Uniswap to get it during metacall - assertEq(DAI.balanceOf(address(uniswapSolver)), 0, "Solver has DAI before metacall"); - - // NOTE: Should metacall return something? Feels like a lot of data you might want to know about the tx - atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp }); - vm.stopPrank(); - - console.log("\nAFTER METACALL"); - console.log("User WETH balance", WETH.balanceOf(userEOA)); - console.log("User DAI balance", DAI.balanceOf(userEOA)); - console.log("Solver WETH balance", WETH.balanceOf(address(uniswapSolver))); - console.log("Solver DAI balance", DAI.balanceOf(address(uniswapSolver))); - - // Check user token balances after - assertEq( - WETH.balanceOf(userEOA), userWethBalanceBefore - swapIntent.amountUserSells, "Did not spend enough WETH" - ); - assertEq(DAI.balanceOf(userEOA), userDaiBalanceBefore + swapIntent.amountUserBuys, "Did not receive enough DAI"); - } -} - -// This solver magically has the tokens needed to fulfil the user's swap. -// This might involve an offchain RFQ system -contract SimpleRFQSolver is SolverBase { - constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { } - - function fulfillRFQ(SwapIntent calldata swapIntent, address executionEnvironment) public { - require( - IERC20(swapIntent.tokenUserSells).balanceOf(address(this)) >= swapIntent.amountUserSells, - "Did not receive enough tokenIn" - ); - require( - IERC20(swapIntent.tokenUserBuys).balanceOf(address(this)) >= swapIntent.amountUserBuys, - "Not enough tokenOut to fulfill" - ); - IERC20(swapIntent.tokenUserBuys).transfer(executionEnvironment, swapIntent.amountUserBuys); - } - - // This ensures a function can only be called through atlasSolverCall - // which includes security checks to work safely with Atlas - modifier onlySelf() { - require(msg.sender == address(this), "Not called via atlasSolverCall"); - _; - } - - fallback() external payable { } - receive() external payable { } -} - -contract UniswapIntentSolver is SolverBase { - IUniV2Router02 router = IUniV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); - - constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { } - - function fulfillWithSwap(SwapIntent calldata swapIntent, address executionEnvironment) public onlySelf { - // Checks recieved expected tokens from Atlas on behalf of user to swap - require( - IERC20(swapIntent.tokenUserSells).balanceOf(address(this)) >= swapIntent.amountUserSells, - "Did not receive enough tokenIn" - ); - - address[] memory path = new address[](2); - path[0] = swapIntent.tokenUserSells; - path[1] = swapIntent.tokenUserBuys; - - // Attempt to sell all tokens for as many as possible of tokenUserBuys - IERC20(swapIntent.tokenUserSells).approve(address(router), swapIntent.amountUserSells); - router.swapExactTokensForTokens({ - amountIn: swapIntent.amountUserSells, - amountOutMin: swapIntent.amountUserBuys, // will revert here if not enough to fulfill intent - path: path, - to: address(this), - deadline: block.timestamp - }); - - // Send min tokens back to user to fulfill intent, rest are profit for solver - IERC20(swapIntent.tokenUserBuys).transfer(executionEnvironment, swapIntent.amountUserBuys); - } - - // This ensures a function can only be called through atlasSolverCall - // which includes security checks to work safely with Atlas - modifier onlySelf() { - require(msg.sender == address(this), "Not called via atlasSolverCall"); - _; - } - - fallback() external payable { } - receive() external payable { } -} - -contract UserCondition { - bool valid = true; - - function enable() external { - valid = true; - } - - function disable() external { - valid = false; - } - - function isLessThanFive(uint256 n) external view returns (bool) { - return valid && n < 5; - } -} diff --git a/test/SwapIntentInvertBid.t.sol b/test/SwapIntentInvertBid.t.sol deleted file mode 100644 index 4d8e8abe3..000000000 --- a/test/SwapIntentInvertBid.t.sol +++ /dev/null @@ -1,281 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import "forge-std/Test.sol"; -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { BaseTest } from "./base/BaseTest.t.sol"; -import { TxBuilder } from "src/contracts/helpers/TxBuilder.sol"; -import { SolverOperation } from "src/contracts/types/SolverOperation.sol"; -import { UserOperation } from "src/contracts/types/UserOperation.sol"; -import { DAppConfig } from "src/contracts/types/ConfigTypes.sol"; -import { SwapIntent, SwapIntentInvertBidDAppControl } from "src/contracts/examples/intents-example/SwapIntentInvertBidDAppControl.sol"; -import { SolverBaseInvertBid } from "src/contracts/solver/SolverBaseInvertBid.sol"; -import { DAppControl } from "src/contracts/dapp/DAppControl.sol"; -import { CallConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/LockTypes.sol"; -import "src/contracts/types/DAppOperation.sol"; - -contract SwapIntentTest is BaseTest { - Sig public sig; - - function setUp() public virtual override { - BaseTest.setUp(); - - deal(WETH_ADDRESS, userEOA, 10e18); - } - - function testAtlasSwapIntentInvertBid_solverBidRetreivalNotRequired_SkipCoverage() public { - vm.startPrank(governanceEOA); - SwapIntentInvertBidDAppControl controlContract = new SwapIntentInvertBidDAppControl(address(atlas), false); - address control = address(controlContract); - atlasVerification.initializeGovernance(control); - vm.stopPrank(); - - uint256 amountUserBuys = 20e18; - uint256 maxAmountUserSells = 10e18; - uint256 solverBidAmount = 1e18; - - SwapIntent memory swapIntent = createSwapIntent(amountUserBuys, maxAmountUserSells); - SimpleRFQSolverInvertBid rfqSolver = deployAndFundRFQSolver(swapIntent, false); - address executionEnvironment = createExecutionEnvironment(control); - UserOperation memory userOp = buildUserOperation(control, swapIntent); - SolverOperation memory solverOp = buildSolverOperation(control, userOp, swapIntent, executionEnvironment, address(rfqSolver), solverBidAmount); - SolverOperation[] memory solverOps = new SolverOperation[](1); - solverOps[0] = solverOp; - DAppOperation memory dAppOp = buildDAppOperation(control, userOp, solverOps); - - uint256 userWethBalanceBefore = WETH.balanceOf(userEOA); - uint256 userDaiBalanceBefore = DAI.balanceOf(userEOA); - - vm.prank(userEOA); - WETH.transfer(address(1), userWethBalanceBefore - swapIntent.maxAmountUserSells); - userWethBalanceBefore = WETH.balanceOf(userEOA); - assertTrue(userWethBalanceBefore >= swapIntent.maxAmountUserSells, "Not enough starting WETH"); - - approveAtlasAndExecuteSwap(swapIntent, userOp, solverOps, dAppOp); - - // Check user token balances after - assertEq(WETH.balanceOf(userEOA), userWethBalanceBefore - solverBidAmount, "Did not spend WETH == solverBidAmount"); - assertEq(DAI.balanceOf(userEOA), userDaiBalanceBefore + swapIntent.amountUserBuys, "Did not receive enough DAI"); - } - - function testAtlasSwapIntentInvertBid_solverBidRetreivalNotRequired_multipleSolvers_SkipCoverage() public { - vm.startPrank(governanceEOA); - SwapIntentInvertBidDAppControl controlContract = new SwapIntentInvertBidDAppControl(address(atlas), false); - address control = address(controlContract); - atlasVerification.initializeGovernance(control); - vm.stopPrank(); - - uint256 amountUserBuys = 20e18; - uint256 maxAmountUserSells = 10e18; - - uint256 solverBidAmountOne = 1e18; - uint256 solverBidAmountTwo = 2e18; - - SwapIntent memory swapIntent = createSwapIntent(amountUserBuys, maxAmountUserSells); - SimpleRFQSolverInvertBid rfqSolver = deployAndFundRFQSolver(swapIntent, false); - address executionEnvironment = createExecutionEnvironment(control); - UserOperation memory userOp = buildUserOperation(control, swapIntent); - SolverOperation memory solverOpOne = buildSolverOperation(control, userOp, swapIntent, executionEnvironment, address(rfqSolver), solverBidAmountOne); - SolverOperation memory solverOpTwo = buildSolverOperation(control, userOp, swapIntent, executionEnvironment, address(rfqSolver), solverBidAmountTwo); - - SolverOperation[] memory solverOps = new SolverOperation[](2); - solverOps[0] = solverOpOne; - solverOps[1] = solverOpTwo; - DAppOperation memory dAppOp = buildDAppOperation(control, userOp, solverOps); - - uint256 userWethBalanceBefore = WETH.balanceOf(userEOA); - uint256 userDaiBalanceBefore = DAI.balanceOf(userEOA); - - vm.prank(userEOA); - WETH.transfer(address(1), userWethBalanceBefore - swapIntent.maxAmountUserSells); - userWethBalanceBefore = WETH.balanceOf(userEOA); - assertTrue(userWethBalanceBefore >= swapIntent.maxAmountUserSells, "Not enough starting WETH"); - - approveAtlasAndExecuteSwap(swapIntent, userOp, solverOps, dAppOp); - - // Check user token balances after - assertEq(WETH.balanceOf(userEOA), userWethBalanceBefore - solverBidAmountOne, "Did not spend WETH == solverBidAmountOne"); - assertEq(DAI.balanceOf(userEOA), userDaiBalanceBefore + swapIntent.amountUserBuys, "Did not receive enough DAI"); - } - - function testAtlasSwapIntentInvertBid_solverBidRetreivalRequired_SkipCoverage() public { - vm.startPrank(governanceEOA); - SwapIntentInvertBidDAppControl controlContract = new SwapIntentInvertBidDAppControl(address(atlas), true); - address control = address(controlContract); - atlasVerification.initializeGovernance(control); - vm.stopPrank(); - - uint256 amountUserBuys = 20e18; - uint256 maxAmountUserSells = 10e18; - uint256 solverBidAmount = 1e18; - - SwapIntent memory swapIntent = createSwapIntent(amountUserBuys, maxAmountUserSells); - SimpleRFQSolverInvertBid rfqSolver = deployAndFundRFQSolver(swapIntent, true); - address executionEnvironment = createExecutionEnvironment(control); - UserOperation memory userOp = buildUserOperation(control, swapIntent); - SolverOperation memory solverOp = buildSolverOperation(control, userOp, swapIntent, executionEnvironment, address(rfqSolver), solverBidAmount); - SolverOperation[] memory solverOps = new SolverOperation[](1); - solverOps[0] = solverOp; - DAppOperation memory dAppOp = buildDAppOperation(control, userOp, solverOps); - - uint256 userWethBalanceBefore = WETH.balanceOf(userEOA); - uint256 userDaiBalanceBefore = DAI.balanceOf(userEOA); - - vm.prank(userEOA); - WETH.transfer(address(1), userWethBalanceBefore - swapIntent.maxAmountUserSells); - userWethBalanceBefore = WETH.balanceOf(userEOA); - assertTrue(userWethBalanceBefore >= swapIntent.maxAmountUserSells, "Not enough starting WETH"); - - approveAtlasAndExecuteSwap(swapIntent, userOp, solverOps, dAppOp); - - // Check user token balances after - assertEq(WETH.balanceOf(userEOA), userWethBalanceBefore - solverBidAmount, "Did not spend WETH == solverBidAmount"); - assertEq(DAI.balanceOf(userEOA), userDaiBalanceBefore + swapIntent.amountUserBuys, "Did not receive enough DAI"); - } - - function createSwapIntent(uint256 amountUserBuys, uint256 maxAmountUserSells) internal view returns (SwapIntent memory) { - return SwapIntent({ - tokenUserBuys: DAI_ADDRESS, - amountUserBuys: amountUserBuys, - tokenUserSells: WETH_ADDRESS, - maxAmountUserSells: maxAmountUserSells - }); - } - - function deployAndFundRFQSolver(SwapIntent memory swapIntent, bool solverBidRetreivalRequired) internal returns (SimpleRFQSolverInvertBid) { - vm.startPrank(solverOneEOA); - SimpleRFQSolverInvertBid rfqSolver = new SimpleRFQSolverInvertBid(WETH_ADDRESS, address(atlas), solverBidRetreivalRequired); - atlas.deposit{ value: 1e18 }(); - atlas.bond(1 ether); - vm.stopPrank(); - - deal(DAI_ADDRESS, address(rfqSolver), swapIntent.amountUserBuys); - assertEq(DAI.balanceOf(address(rfqSolver)), swapIntent.amountUserBuys, "Did not give enough DAI to solver"); - - return rfqSolver; - } - - function createExecutionEnvironment(address control) internal returns (address){ - vm.startPrank(userEOA); - address executionEnvironment = atlas.createExecutionEnvironment(userEOA, control); - console.log("executionEnvironment", executionEnvironment); - vm.stopPrank(); - vm.label(address(executionEnvironment), "EXECUTION ENV"); - - return executionEnvironment; - } - - function buildUserOperation(address control, SwapIntent memory swapIntent) internal returns (UserOperation memory) { - UserOperation memory userOp; - - bytes memory userOpData = abi.encodeCall(SwapIntentInvertBidDAppControl.swap, swapIntent); - - TxBuilder txBuilder = new TxBuilder({ - _control: control, - _atlas: address(atlas), - _verification: address(atlasVerification) - }); - - userOp = txBuilder.buildUserOperation({ - from: userEOA, - to: txBuilder.control(), - maxFeePerGas: tx.gasprice + 1, - value: 0, - deadline: block.number + 2, - data: userOpData - }); - userOp.sessionKey = governanceEOA; - - return userOp; - } - - function buildSolverOperation(address control, UserOperation memory userOp, SwapIntent memory swapIntent, address executionEnvironment, - address solverAddress, uint256 solverBidAmount) internal returns (SolverOperation memory) { - bytes memory solverOpData = - abi.encodeCall(SimpleRFQSolverInvertBid.fulfillRFQ, (swapIntent, executionEnvironment, solverBidAmount)); - - TxBuilder txBuilder = new TxBuilder({ - _control: control, - _atlas: address(atlas), - _verification: address(atlasVerification) - }); - - SolverOperation memory solverOp = txBuilder.buildSolverOperation({ - userOp: userOp, - solverOpData: solverOpData, - solver: solverOneEOA, - solverContract: solverAddress, - bidAmount: solverBidAmount, - value: 0 - }); - - (sig.v, sig.r, sig.s) = vm.sign(solverOnePK, atlasVerification.getSolverPayload(solverOp)); - solverOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - return solverOp; - } - - function buildDAppOperation(address control, UserOperation memory userOp, SolverOperation[] memory solverOps) - internal returns (DAppOperation memory) { - TxBuilder txBuilder = new TxBuilder({ - _control: control, - _atlas: address(atlas), - _verification: address(atlasVerification) - }); - DAppOperation memory dAppOp = txBuilder.buildDAppOperation(governanceEOA, userOp, solverOps); - - (sig.v, sig.r, sig.s) = vm.sign(governancePK, atlasVerification.getDAppOperationPayload(dAppOp)); - dAppOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - return dAppOp; - } - - function approveAtlasAndExecuteSwap(SwapIntent memory swapIntent, UserOperation memory userOp, SolverOperation[] memory solverOps, DAppOperation memory dAppOp) internal { - vm.startPrank(userEOA); - - // (bool simResult,,) = simulator.simUserOperation(userOp); - // assertFalse(simResult, "metasimUserOperationcall tested true a"); - - WETH.approve(address(atlas), swapIntent.maxAmountUserSells); - - (bool simResult,,) = simulator.simUserOperation(userOp); - assertTrue(simResult, "metasimUserOperationcall tested false c"); - uint256 gasLeftBefore = gasleft(); - - vm.startPrank(userEOA); - atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp }); - - console.log("Metacall Gas Cost:", gasLeftBefore - gasleft()); - vm.stopPrank(); - } -} - -// This solver magically has the tokens needed to fulfil the user's swap. -// This might involve an offchain RFQ system -contract SimpleRFQSolverInvertBid is SolverBaseInvertBid { - constructor(address weth, address atlas, bool solverBidRetrivalRequired) SolverBaseInvertBid(weth, atlas, msg.sender, solverBidRetrivalRequired) { } - - function fulfillRFQ(SwapIntent calldata swapIntent, address executionEnvironment, uint256 solverBidAmount) public { - require( - IERC20(swapIntent.tokenUserSells).balanceOf(address(this)) >= solverBidAmount, - "Did not receive enough tokenUserSells (=solverBidAmount) to fulfill swapIntent" - ); - require( - IERC20(swapIntent.tokenUserBuys).balanceOf(address(this)) >= swapIntent.amountUserBuys, - "Not enough tokenUserBuys to fulfill" - ); - IERC20(swapIntent.tokenUserBuys).transfer(executionEnvironment, swapIntent.amountUserBuys); - } - - // This ensures a function can only be called through atlasSolverCall - // which includes security checks to work safely with Atlas - modifier onlySelf() { - require(msg.sender == address(this), "Not called via atlasSolverCall"); - _; - } - - fallback() external payable { } - receive() external payable { } -} \ No newline at end of file diff --git a/test/TrebleSwap.t.sol b/test/TrebleSwap.t.sol deleted file mode 100644 index 9ac734205..000000000 --- a/test/TrebleSwap.t.sol +++ /dev/null @@ -1,574 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import "forge-std/Test.sol"; - -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -import { SolverOperation } from "src/contracts/types/SolverOperation.sol"; -import { UserOperation } from "src/contracts/types/UserOperation.sol"; -import { DAppConfig } from "src/contracts/types/ConfigTypes.sol"; -import { DAppOperation } from "src/contracts/types/DAppOperation.sol"; -import { CallVerification } from "src/contracts/libraries/CallVerification.sol"; - -import { SolverBase } from "src/contracts/solver/SolverBase.sol"; - -import { BaseTest } from "./base/BaseTest.t.sol"; -import { TrebleSwapDAppControl } from "src/contracts/examples/trebleswap/TrebleSwapDAppControl.sol"; - -contract TrebleSwapTest is BaseTest { - struct SwapTokenInfo { - address inputToken; - uint256 inputAmount; - address outputToken; - uint256 outputMin; - } - - struct Args { - UserOperation userOp; - SolverOperation[] solverOps; - DAppOperation dAppOp; - uint256 blockBefore; // block before real tx happened - bool nativeInput; - bool nativeOutput; - } - - struct BeforeAndAfterVars { - uint256 userInputTokenBalance; - uint256 userOutputTokenBalance; - uint256 solverTrebBalance; - uint256 burnAddressTrebBalance; - uint256 atlasGasSurcharge; - } - - // Base addresses - address ODOS_ROUTER = 0x19cEeAd7105607Cd444F5ad10dd51356436095a1; - address BURN = address(0xdead); - address ETH = address(0); - address bWETH = 0x4200000000000000000000000000000000000006; - address USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; - address WUF = 0x4da78059D97f155E18B37765e2e042270f4E0fC4; - address bDAI = 0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb; - address BRETT = 0x532f27101965dd16442E59d40670FaF5eBB142E4; - address TREB; // will be set to value in DAppControl in setUp - - uint256 ERR_MARGIN = 0.18e18; // 18% error margin - uint256 bundlerGasEth = 1e16; - - TrebleSwapDAppControl trebleSwapControl; - address executionEnvironment; - - Sig sig; - Args args; - SwapTokenInfo swapInfo; - BeforeAndAfterVars beforeVars; - - function setUp() public virtual override { - // Fork Base - vm.createSelectFork(vm.envString("BASE_RPC_URL"), 18_906_794); - __createAndLabelAccounts(); - __deployAtlasContracts(); - __fundSolversAndDepositAtlETH(); - - vm.startPrank(governanceEOA); - trebleSwapControl = new TrebleSwapDAppControl(address(atlas)); - atlasVerification.initializeGovernance(address(trebleSwapControl)); - vm.stopPrank(); - - vm.prank(userEOA); - executionEnvironment = atlas.createExecutionEnvironment(userEOA, address(trebleSwapControl)); - - TREB = trebleSwapControl.TREB(); - - vm.label(bWETH, "WETH"); - vm.label(USDC, "USDC"); - vm.label(WUF, "WUF"); - vm.label(bDAI, "DAI"); - vm.label(TREB, "TREB"); - } - - // ---------------------------------------------------- // - // Scenario Tests // - // ---------------------------------------------------- // - - function testTrebleSwap_Metacall_Erc20ToErc20_ZeroSolvers() public { - // Tx: https://basescan.org/tx/0x0ef4a9c24bbede2b39e12f5e5417733fa8183f372e41ee099c2c7523064c1b55 - // Swaps 197.2 USDC for at least 198,080,836.0295 WUF - - args.blockBefore = 18_906_794; - args.nativeInput = false; - args.nativeOutput = false; - swapInfo = SwapTokenInfo({ - inputToken: USDC, - inputAmount: 197_200_000, - outputToken: WUF, - outputMin: 1_980_808_360_295 - }); - vm.roll(args.blockBefore); - - bytes memory userOpData = _buildUserOpData("Erc20ToErc20"); - _checkActualCalldataMatchesExpected(userOpData); - _buildUserOp(userOpData); - // no solverOps - _buildDAppOp(); - - _setBalancesAndApprovals(); - _checkSimulationsPass(); - _doMetacallAndChecks({ winningSolver: address(0) }); - } - - function testTrebleSwap_Metacall_Erc20ToErc20_OneSolver() public { - // Tx: https://basescan.org/tx/0x0ef4a9c24bbede2b39e12f5e5417733fa8183f372e41ee099c2c7523064c1b55 - // Swaps 197.2 USDC for at least 198,080,836.0295 WUF - - args.blockBefore = 18_906_794; - args.nativeInput = false; - args.nativeOutput = false; - swapInfo = SwapTokenInfo({ - inputToken: USDC, - inputAmount: 197_200_000, - outputToken: WUF, - outputMin: 1_980_808_360_295 - }); - vm.roll(args.blockBefore); - - bytes memory userOpData = _buildUserOpData("Erc20ToErc20"); - _checkActualCalldataMatchesExpected(userOpData); - _buildUserOp(userOpData); - address solverContract = _setUpSolver(solverOneEOA, solverOnePK, 1e18); - _buildDAppOp(); - - _setBalancesAndApprovals(); - _checkSimulationsPass(); - _doMetacallAndChecks({ winningSolver: solverContract }); - } - - function testTrebleSwap_Metacall_Erc20ToErc20_SwapsEvenIfSolverFails() public { - // Tx: https://basescan.org/tx/0x0ef4a9c24bbede2b39e12f5e5417733fa8183f372e41ee099c2c7523064c1b55 - // Swaps 197.2 USDC for at least 198,080,836.0295 WUF - - args.blockBefore = 18_906_794; - args.nativeInput = false; - args.nativeOutput = false; - swapInfo = SwapTokenInfo({ - inputToken: USDC, - inputAmount: 197_200_000, - outputToken: WUF, - outputMin: 1_980_808_360_295 - }); - vm.roll(args.blockBefore); - - bytes memory userOpData = _buildUserOpData("Erc20ToErc20"); - _checkActualCalldataMatchesExpected(userOpData); - _buildUserOp(userOpData); - address solverContract = _setUpSolver(solverOneEOA, solverOnePK, 1e18); - _buildDAppOp(); - - _setBalancesAndApprovals(); - _checkSimulationsPass(); - - // Set solver to fail during metacall, user swap should still go through - MockTrebleSolver(payable(solverContract)).setShouldSucceed(false); - - _doMetacallAndChecks({ winningSolver: address(0) }); - } - - function testTrebleSwap_Metacall_EthToErc20_ZeroSolvers() public { - // Tx: https://basescan.org/tx/0xe138def4155bea056936038b9374546a366828ab8bf1233056f9e2fe4c6af999 - // Swaps 0.123011147164483512 ETH for at least 307.405807527716546728 DAI - - args.blockBefore = 19_026_442; - args.nativeInput = true; - args.nativeOutput = false; - swapInfo = SwapTokenInfo({ - inputToken: ETH, - inputAmount: 123_011_147_164_483_512, - outputToken: bDAI, - outputMin: 307_405_807_527_716_546_728 - }); - vm.roll(args.blockBefore); - - bytes memory userOpData = _buildUserOpData("EthToErc20"); - _checkActualCalldataMatchesExpected(userOpData); - _buildUserOp(userOpData); - // no solverOps - _buildDAppOp(); - - _setBalancesAndApprovals(); - _checkSimulationsPass(); - _doMetacallAndChecks({ winningSolver: address(0) }); - } - - function testTrebleSwap_Metacall_EthToErc20_OneSolver() public { - // Tx: https://basescan.org/tx/0xe138def4155bea056936038b9374546a366828ab8bf1233056f9e2fe4c6af999 - // Swaps 0.123011147164483512 ETH for at least 307.405807527716546728 DAI - - args.blockBefore = 19_026_442; - args.nativeInput = true; - args.nativeOutput = false; - swapInfo = SwapTokenInfo({ - inputToken: ETH, - inputAmount: 123_011_147_164_483_512, - outputToken: bDAI, - outputMin: 307_405_807_527_716_546_728 - }); - vm.roll(args.blockBefore); - - bytes memory userOpData = _buildUserOpData("EthToErc20"); - _checkActualCalldataMatchesExpected(userOpData); - _buildUserOp(userOpData); - address solverContract = _setUpSolver(solverOneEOA, solverOnePK, 1e18); - _buildDAppOp(); - - _setBalancesAndApprovals(); - _checkSimulationsPass(); - _doMetacallAndChecks({ winningSolver: solverContract }); - } - - function testTrebleSwap_Metacall_Erc20ToEth_ZeroSolvers() public { - // https://basescan.org/tx/0xaf26570fceddf2d21219a9e03f2cfee52c600a40ddfdfc5d82eff14f3d322f8f - // Swaps 24831.337726043809120256 BRETT for at least 0.786534993470006277 ETH - - args.blockBefore = 19_044_388; - args.nativeInput = false; - args.nativeOutput = true; - swapInfo = SwapTokenInfo({ - inputToken: BRETT, - inputAmount: 24_831_337_726_043_809_120_256, - outputToken: ETH, - outputMin: 786_534_993_470_006_277 - }); - vm.roll(args.blockBefore); - - bytes memory userOpData = _buildUserOpData("Erc20ToEth"); - _checkActualCalldataMatchesExpected(userOpData); - _buildUserOp(userOpData); - // no solverOps - _buildDAppOp(); - - _setBalancesAndApprovals(); - _checkSimulationsPass(); - _doMetacallAndChecks({ winningSolver: address(0) }); - } - - function testTrebleSwap_Metacall_Erc20ToEth_OneSolver() public { - // https://basescan.org/tx/0xaf26570fceddf2d21219a9e03f2cfee52c600a40ddfdfc5d82eff14f3d322f8f - // Swaps 24831.337726043809120256 BRETT for at least 0.786534993470006277 ETH - - args.blockBefore = 19_044_388; - args.nativeInput = false; - args.nativeOutput = true; - swapInfo = SwapTokenInfo({ - inputToken: BRETT, - inputAmount: 24_831_337_726_043_809_120_256, - outputToken: ETH, - outputMin: 786_534_993_470_006_277 - }); - vm.roll(args.blockBefore); - - bytes memory userOpData = _buildUserOpData("Erc20ToEth"); - _checkActualCalldataMatchesExpected(userOpData); - _buildUserOp(userOpData); - address solverContract = _setUpSolver(solverOneEOA, solverOnePK, 1e18); - _buildDAppOp(); - - _setBalancesAndApprovals(); - _checkSimulationsPass(); - _doMetacallAndChecks({ winningSolver: solverContract }); - } - - // ---------------------------------------------------- // - // Helper Functions // - // ---------------------------------------------------- // - - function _doMetacallAndChecks(address winningSolver) internal { - bool auctionWonExpected = winningSolver != address(0); - beforeVars.userInputTokenBalance = _balanceOf(swapInfo.inputToken, userEOA); - beforeVars.userOutputTokenBalance = _balanceOf(swapInfo.outputToken, userEOA); - beforeVars.solverTrebBalance = _balanceOf(address(TREB), winningSolver); - beforeVars.burnAddressTrebBalance = _balanceOf(address(TREB), BURN); - beforeVars.atlasGasSurcharge = atlas.cumulativeSurcharge(); - uint256 msgValue = (args.nativeInput ? swapInfo.inputAmount : 0) + bundlerGasEth; - if (args.nativeInput) beforeVars.userInputTokenBalance -= bundlerGasEth; - if (args.nativeOutput) beforeVars.userOutputTokenBalance -= bundlerGasEth; - - uint256 txGasUsed; - uint256 estAtlasGasSurcharge = gasleft(); // Reused below during calculations - - // Do the actual metacall - vm.prank(userEOA); - bool auctionWon = atlas.metacall{ value: msgValue }(args.userOp, args.solverOps, args.dAppOp); - - // Estimate gas surcharge Atlas should have taken - txGasUsed = estAtlasGasSurcharge - gasleft(); - estAtlasGasSurcharge = txGasUsed * tx.gasprice * atlas.ATLAS_SURCHARGE_RATE() / atlas.SCALE(); - - // Check Atlas auctionWon return value - assertEq(auctionWon, auctionWonExpected, "auctionWon not as expected"); - - // Check Atlas gas surcharge change - assertApproxEqRel( - atlas.cumulativeSurcharge() - beforeVars.atlasGasSurcharge, - estAtlasGasSurcharge, - ERR_MARGIN, - "Atlas gas surcharge not within estimated range" - ); - - // Check user input token change - if (args.nativeInput && auctionWonExpected) { - // solver will refund some bundler ETH to user, throwing off ETH balance - uint256 buffer = 1e17; // 0.1 ETH buffer as base for error margin comparison - uint256 expectedBalanceAfter = beforeVars.userInputTokenBalance - swapInfo.inputAmount; - assertApproxEqRel( - _balanceOf(swapInfo.inputToken, userEOA) + buffer, - expectedBalanceAfter + buffer, - 0.01e18, // error marin: 1% of the 0.1 ETH buffer - "wrong user input token (ETH) balance change" - ); - } else { - assertEq( - _balanceOf(swapInfo.inputToken, userEOA), - beforeVars.userInputTokenBalance - swapInfo.inputAmount, - "wrong user input token (ERC20/ETH) balance change" - ); - } - - // Check user output token change - assertTrue( - _balanceOf(swapInfo.outputToken, userEOA) >= beforeVars.userOutputTokenBalance + swapInfo.outputMin, - "wrong user output token balance change" - ); - - // Check solver and burn address TREB balance change - if (auctionWonExpected) { - // Solver TREB decreased by bidAmount - assertEq( - _balanceOf(address(TREB), winningSolver), - beforeVars.solverTrebBalance - args.solverOps[0].bidAmount, - "wrong solver TREB balance change" - ); - // Burn address TREB increased by bidAmount - assertEq( - _balanceOf(address(TREB), BURN), - beforeVars.burnAddressTrebBalance + args.solverOps[0].bidAmount, - "wrong burn address TREB balance change" - ); - } else { - // No change in solver TREB - assertEq( - _balanceOf(address(TREB), winningSolver), beforeVars.solverTrebBalance, "solver TREB balance changed" - ); - // No change in burn address TREB - assertEq( - _balanceOf(address(TREB), BURN), beforeVars.burnAddressTrebBalance, "burn address TREB balance changed" - ); - } - } - - function _checkSimulationsPass() internal { - bool success; - - (success,,) = simulator.simUserOperation(args.userOp); - assertEq(success, true, "simUserOperation failed"); - - if (args.solverOps.length > 0) { - (success,,) = simulator.simSolverCalls(args.userOp, args.solverOps, args.dAppOp); - assertEq(success, true, "simSolverCalls failed"); - } - } - - function _checkActualCalldataMatchesExpected(bytes memory userOpData) internal view { - bytes memory encodedCall = abi.encodePacked(TrebleSwapDAppControl.decodeUserOpData.selector, userOpData); - (bool res, bytes memory returnData) = address(trebleSwapControl).staticcall(encodedCall); - assertEq(res, true, "calldata check failed in decode call"); - - SwapTokenInfo memory decodedInfo = abi.decode(returnData, (SwapTokenInfo)); - assertEq(decodedInfo.inputToken, swapInfo.inputToken, "inputToken mismatch"); - assertEq(decodedInfo.inputAmount, swapInfo.inputAmount, "inputAmount mismatch"); - assertEq(decodedInfo.outputToken, swapInfo.outputToken, "outputToken mismatch"); - assertEq(decodedInfo.outputMin, swapInfo.outputMin, "outputMin mismatch"); - } - - function _setBalancesAndApprovals() internal { - // User input token and Atlas approval - if (args.nativeInput) { - deal(userEOA, swapInfo.inputAmount + bundlerGasEth); - } else { - deal(userEOA, bundlerGasEth); - deal(swapInfo.inputToken, userEOA, swapInfo.inputAmount); - vm.prank(userEOA); - IERC20(swapInfo.inputToken).approve(address(atlas), swapInfo.inputAmount); - } - - // Give bidAmount of TREB to solver contract for each solverOp - for (uint256 i = 0; i < args.solverOps.length; i++) { - deal(TREB, args.solverOps[i].solver, args.solverOps[i].bidAmount); - } - } - - function _buildUserOp(bytes memory userOpData) internal { - args.userOp = UserOperation({ - from: userEOA, - to: address(atlas), - value: args.nativeInput ? swapInfo.inputAmount : 0, - gas: 1_000_000, - maxFeePerGas: tx.gasprice, - nonce: 1, - deadline: args.blockBefore + 2, - dapp: ODOS_ROUTER, - control: address(trebleSwapControl), - callConfig: trebleSwapControl.CALL_CONFIG(), - sessionKey: address(0), - data: userOpData, - signature: new bytes(0) - }); - } - - function _setUpSolver( - address solverEOA, - uint256 solverPK, - uint256 bidAmount - ) - internal - returns (address solverContract) - { - vm.startPrank(solverEOA); - // Make sure solver has 1 AtlETH bonded in Atlas - uint256 bonded = atlas.balanceOfBonded(solverEOA); - if (bonded < 1e18) { - uint256 atlETHBalance = atlas.balanceOf(solverEOA); - if (atlETHBalance < 1e18) { - deal(solverEOA, 1e18 - atlETHBalance); - atlas.deposit{ value: 1e18 - atlETHBalance }(); - } - atlas.bond(1e18 - bonded); - } - - // Deploy solver contract - MockTrebleSolver solver = new MockTrebleSolver(bWETH, address(atlas)); - - // Create signed solverOp - SolverOperation memory solverOp = _buildSolverOp(solverEOA, solverPK, address(solver), bidAmount); - vm.stopPrank(); - - // add to solverOps array and return solver contract address - args.solverOps.push(solverOp); - return address(solver); - } - - function _buildSolverOp( - address solverEOA, - uint256 solverPK, - address solverContract, - uint256 bidAmount - ) - internal - returns (SolverOperation memory solverOp) - { - solverOp = SolverOperation({ - from: solverEOA, - to: address(atlas), - value: 0, - gas: 100_000, - maxFeePerGas: args.userOp.maxFeePerGas, - deadline: args.userOp.deadline, - solver: solverContract, - control: address(trebleSwapControl), - userOpHash: atlasVerification.getUserOperationHash(args.userOp), - bidToken: trebleSwapControl.getBidFormat(args.userOp), - bidAmount: bidAmount, - data: abi.encodeCall(MockTrebleSolver.solve, ()), - signature: new bytes(0) - }); - // Sign solverOp - (sig.v, sig.r, sig.s) = vm.sign(solverPK, atlasVerification.getSolverPayload(solverOp)); - solverOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - } - - function _buildDAppOp() internal { - args.dAppOp = DAppOperation({ - from: governanceEOA, - to: address(atlas), - nonce: 1, - deadline: args.blockBefore + 2, - control: address(trebleSwapControl), - bundler: address(0), - userOpHash: atlasVerification.getUserOperationHash(args.userOp), - callChainHash: CallVerification.getCallChainHash(args.userOp, args.solverOps), - signature: new bytes(0) - }); - - (sig.v, sig.r, sig.s) = vm.sign(governancePK, atlasVerification.getDAppOperationPayload(args.dAppOp)); - args.dAppOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - } - - function _buildUserOpData(string memory swapType) internal view returns (bytes memory) { - bytes32 typeHash = keccak256(abi.encodePacked(swapType)); - bytes memory calldataPart1; - bytes memory calldataPart2; - if (typeHash == keccak256("Erc20ToErc20")) { - // Tx: https://basescan.org/tx/0x0ef4a9c24bbede2b39e12f5e5417733fa8183f372e41ee099c2c7523064c1b55 - calldataPart1 = - hex"83bd37f9000400014da78059d97f155e18b37765e2e042270f4e0fc4040bc108800601d1d9f50a5a028f5c0001f73f77f9466da712590ae432a80f07fd50a7de600001616535324976f8dbcef19df0705b95ace86ebb480001"; - calldataPart2 = - hex"0000000006020207003401000001020180000005020a0004040500000301010003060119ff0000000000000000000000000000000000000000000000000000000000000000616535324976f8dbcef19df0705b95ace86ebb48833589fcd6edb6e08f4c7c32d4f71b54bda02913569d81c17b5b4ac08929dc1769b8e39668d3ae29f6c0a374a483101e04ef5f7ac9bd15d9142bac95d9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca42000000000000000000000000000000000000060000000000000000"; - } else if (typeHash == keccak256("EthToErc20")) { - // Tx: https://basescan.org/tx/0xe138def4155bea056936038b9374546a366828ab8bf1233056f9e2fe4c6af999 - calldataPart1 = - hex"83bd37f9000000050801b505fc9226ffb80910d5345f06a9650000028f5c0001f73f77f9466da712590ae432a80f07fd50a7de6000000001"; - calldataPart2 = - hex"000000000401020500040101020a000202030100340101000104007ffffffaff00000000005fb33b095c6e739be19364ab408cd8f102262bb672ab388e2e2f6facef59e3c3fa2c4e29011c2d384200000000000000000000000000000000000006833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000"; - } else if (typeHash == keccak256("Erc20ToEth")) { - // https://basescan.org/tx/0xaf26570fceddf2d21219a9e03f2cfee52c600a40ddfdfc5d82eff14f3d322f8f - calldataPart1 = - hex"83bd37f90001532f27101965dd16442e59d40670faf5ebb142e400000a05421c0933c565400000080aed2177322a39000041890001f73f77f9466da712590ae432a80f07fd50a7de6000000001"; - calldataPart2 = - hex"0000000003010204010e6604680a0100010200000a0100030200020400000001ff0000000036a46dff597c5a444bbc521d26787f57867d2214532f27101965dd16442e59d40670faf5ebb142e44e829f8a5213c42535ab84aa40bd4adcce9cba0200000000"; - } else { - revert("Invalid swap type"); - } - return abi.encodePacked(calldataPart1, executionEnvironment, calldataPart2); - } - - function _balanceOf(address token, address account) internal view returns (uint256) { - if (token == ETH) { - return account.balance; - } else { - return IERC20(token).balanceOf(account); - } - } -} - -// Just bids `bidAmount` in TREB token - doesn't do anything else -contract MockTrebleSolver is SolverBase { - bool internal s_shouldSucceed; - - constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { - s_shouldSucceed = true; // should succeed by default, can be set to false - } - - function shouldSucceed() public view returns (bool) { - return s_shouldSucceed; - } - - function setShouldSucceed(bool succeed) public { - s_shouldSucceed = succeed; - } - - function solve() public view onlySelf { - require(s_shouldSucceed, "Solver failed intentionally"); - - // The solver bid representing user's minAmountUserBuys of tokenUserBuys is sent to the - // Execution Environment in the payBids modifier logic which runs after this function ends. - } - - // This ensures a function can only be called through atlasSolverCall - // which includes security checks to work safely with Atlas - modifier onlySelf() { - require(msg.sender == address(this), "Not called via atlasSolverCall"); - _; - } - - fallback() external payable { } - receive() external payable { } -} diff --git a/test/V2Helper.sol b/test/V2Helper.sol deleted file mode 100644 index 34880fead..000000000 --- a/test/V2Helper.sol +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import { TxBuilder } from "src/contracts/helpers/TxBuilder.sol"; - -import { IUniswapV2Pair } from "src/contracts/examples/v2-example/interfaces/IUniswapV2Pair.sol"; - -import { BlindBackrun } from "src/contracts/solver/src/BlindBackrun/BlindBackrun.sol"; - -import "src/contracts/types/SolverOperation.sol"; -import "src/contracts/types/UserOperation.sol"; -import "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/EscrowTypes.sol"; -import "src/contracts/types/LockTypes.sol"; -import "src/contracts/types/ConfigTypes.sol"; - -import "forge-std/Test.sol"; - -contract V2Helper is Test, TxBuilder { - uint256 public immutable maxFeePerGas; - - constructor(address _control, address _atlas, address _verification) TxBuilder(_control, _atlas, _verification) { - maxFeePerGas = tx.gasprice * 2; - } - - function _getTradeAmtAndDirection( - address firstPool, - address secondPool, - address tokenIn - ) - internal - view - returns (uint256 token0Balance, uint256 token1Balance) - { - address token0 = IUniswapV2Pair(firstPool).token0(); - - (uint112 token0Balance_a, uint112 token1Balance_a,) = IUniswapV2Pair(firstPool).getReserves(); - (uint112 token0Balance_b, uint112 token1Balance_b,) = IUniswapV2Pair(secondPool).getReserves(); - - if (token0 != IUniswapV2Pair(secondPool).token0()) { - (token1Balance_b, token0Balance_b,) = IUniswapV2Pair(secondPool).getReserves(); - } - - // get the smaller one - bool flip = token0 == tokenIn; - token0Balance = flip ? 0 : uint256(token0Balance_a > token0Balance_b ? token0Balance_b : token0Balance_a) / 3; - token1Balance = flip ? uint256(token1Balance_a > token1Balance_b ? token1Balance_b : token1Balance_a) / 3 : 0; - } - - function buildUserOperation( - address firstPool, - address secondPool, - address from, - address tokenIn - ) - public - view - returns (UserOperation memory userOp) - { - (uint256 token0Balance, uint256 token1Balance) = _getTradeAmtAndDirection(firstPool, secondPool, tokenIn); - - console.log("-"); - console.log("sell token", tokenIn); - console.log("token0 in ", token0Balance); - console.log("token1 in ", token1Balance); - console.log("-"); - - return TxBuilder.buildUserOperation( - from, firstPool, maxFeePerGas, 0, block.number + 2, buildV2SwapCalldata(token0Balance, token1Balance, from) - ); - } - - function buildV2SwapCalldata( - uint256 amount0Out, - uint256 amount1Out, - address recipient - ) - public - pure - returns (bytes memory data) - { - data = abi.encodeWithSelector(IUniswapV2Pair.swap.selector, amount0Out, amount1Out, recipient, data); - } - - function buildV2SolverOperationData(address poolOne, address poolTwo) public pure returns (bytes memory data) { - data = abi.encodeWithSelector(BlindBackrun.executeArbitrage.selector, poolOne, poolTwo); - } -} diff --git a/test/V2RewardDAppControl.t.sol b/test/V2RewardDAppControl.t.sol deleted file mode 100644 index 62154d346..000000000 --- a/test/V2RewardDAppControl.t.sol +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import "forge-std/Test.sol"; - -import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; - -import { BaseTest } from "test/base/BaseTest.t.sol"; -import { TxBuilder } from "src/contracts/helpers/TxBuilder.sol"; -import { UserOperationBuilder } from "test/base/builders/UserOperationBuilder.sol"; - -import { SolverOperation } from "src/contracts/types/SolverOperation.sol"; -import { UserOperation } from "src/contracts/types/UserOperation.sol"; -import { DAppConfig } from "src/contracts/types/ConfigTypes.sol"; -import "src/contracts/types/DAppOperation.sol"; - -import { V2RewardDAppControl } from "src/contracts/examples/v2-example-router/V2RewardDAppControl.sol"; -import { IUniswapV2Router01, IUniswapV2Router02 } from "src/contracts/examples/v2-example-router/interfaces/IUniswapV2Router.sol"; -import { SolverBase } from "src/contracts/solver/SolverBase.sol"; - -contract V2RewardDAppControlTest is BaseTest { - address V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; - - V2RewardDAppControl v2RewardControl; - TxBuilder txBuilder; - Sig sig; - - BasicV2Solver basicV2Solver; - - function setUp() public override { - super.setUp(); - - vm.startPrank(governanceEOA); - v2RewardControl = new V2RewardDAppControl(address(atlas), WETH_ADDRESS, V2_ROUTER); - atlasVerification.initializeGovernance(address(v2RewardControl)); - vm.stopPrank(); - - txBuilder = new TxBuilder({ - _control: address(v2RewardControl), - _atlas: address(atlas), - _verification: address(atlasVerification) - }); - } - - // Swap 1 WETH for 1830 DAI - function test_V2RewardDApp_swapWETHForDAI() public { - // FIXME: fix before merging spearbit-reaudit branch - vm.skip(true); - // This whole test will get redone in the gas accounting update - - UserOperation memory userOp; - SolverOperation[] memory solverOps = new SolverOperation[](1); - DAppOperation memory dAppOp; - - // USER STUFF - - vm.startPrank(userEOA); - address executionEnvironment = atlas.createExecutionEnvironment(userEOA, address(v2RewardControl)); - console.log("Execution Environment:", executionEnvironment); - vm.stopPrank(); - vm.label(address(executionEnvironment), "EXECUTION ENV"); - - address[] memory path = new address[](2); - path[0] = WETH_ADDRESS; - path[1] = DAI_ADDRESS; - bytes memory userOpData = abi.encodeCall(IUniswapV2Router01.swapExactTokensForTokens, ( - 1e18, // amountIn - 0, // amountOutMin - path, // path - userEOA, // to - block.timestamp + 999 // timestamp deadline - )); - - userOp = txBuilder.buildUserOperation({ - from: userEOA, - to: address(v2RewardControl), - maxFeePerGas: tx.gasprice + 1, - value: 0, - deadline: block.number + 555, // block deadline - data: userOpData - }); - - // Exec Env calls swapExactTokensForTokens on Uni V2 Router directly - userOp.dapp = V2_ROUTER; - userOp.sessionKey = governanceEOA; - - // User signs UserOperation data - (sig.v, sig.r, sig.s) = vm.sign(userPK, atlasVerification.getUserOperationPayload(userOp)); - userOp.signature = abi.encodePacked(sig.r, sig.s, sig.v); - - vm.startPrank(userEOA); - WETH.transfer(address(1), WETH.balanceOf(userEOA) - 2e18); // Burn WETH to make logs readable - WETH.approve(address(atlas), 1e18); // approve Atlas to take WETH for swap - vm.stopPrank(); - - // SOLVER STUFF - SOLVER NOT NEEDED IF BUNDLER (DAPP) PAYS GAS - - // vm.startPrank(solverOneEOA); - // basicV2Solver = new BasicV2Solver(WETH_ADDRESS, address(atlas)); - // atlas.deposit{ value: 1e18 }(); - // atlas.bond(1e18); - // vm.stopPrank(); - - // bytes memory solverOpData = abi.encodeWithSelector(BasicV2Solver.backrun.selector); - // solverOps[0] = txBuilder.buildSolverOperation({ - // userOp: userOp, - // solverOpData: solverOpData, - // solverEOA: solverOneEOA, - // solverContract: address(basicV2Solver), - // bidAmount: 1e17, // 0.1 ETH - // value: 0 - // }); - - // (sig.v, sig.r, sig.s) = vm.sign(solverOnePK, atlasVerification.getSolverPayload(solverOps[0])); - // solverOps[0].signature = abi.encodePacked(sig.r, sig.s, sig.v); - - // DAPP STUFF - dAppOp = txBuilder.buildDAppOperation(governanceEOA, userOp, solverOps); - - // DApp Gov bonds AtlETH to pay gas in event of no solver - deal(governanceEOA, 2e18); - vm.startPrank(governanceEOA); - atlas.deposit{ value: 1e18 }(); - atlas.bond(1e18); - vm.stopPrank(); - - // METACALL STUFF - - console.log("\nBEFORE METACALL"); - console.log("User WETH balance", WETH.balanceOf(userEOA)); - console.log("User DAI balance", DAI.balanceOf(userEOA)); - - vm.prank(governanceEOA); - atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp }); - - console.log("\nAFTER METACALL"); - console.log("User WETH balance", WETH.balanceOf(userEOA)); - console.log("User DAI balance", DAI.balanceOf(userEOA)); - } -} - -contract BasicV2Solver is SolverBase { - constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { } - - function backrun() public onlySelf { - // Backrun logic would go here - } - - // This ensures a function can only be called through atlasSolverCall - // which includes security checks to work safely with Atlas - modifier onlySelf() { - require(msg.sender == address(this), "Not called via atlasSolverCall"); - _; - } - - fallback() external payable { } - receive() external payable { } -} \ No newline at end of file