diff --git a/AtlasFlow.jpeg b/AtlasFlow.jpeg new file mode 100644 index 00000000..ba97d5ff Binary files /dev/null and b/AtlasFlow.jpeg differ diff --git a/README.md b/README.md index 1e4e5fe1..51727dd1 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,24 @@ Atlas is infrastructure-agnostic; each DApp may choose how the DApp-designated b 3. **BloXroute**: When Atlas is launched, BloXroute's BDN will support the aggregation of User and Solver Operations for rapid bundling. 4. **SUAVE**: Once live, Operations can be sent to the SUAVE network, bundled into a transaction by the SUAVE Atlas implementation, and then made available for use by builders. +### Auctioneer Overview + +Each DApp may choose a party to act as a trusted auctioneer. **It is strongly recommended that the DApp select the auction beneficiary act as the auctioneer.** The beneficiary can always trust themselves and this prevents adding new, trusted parties. We expect most -but not all- DApps to select the User as the auctioneer and to handle the auctioneer duties without User input through the frontend, which the User already trusts explicitly. + +The auctioneer is tasked with signing a **DAppOperation** that includes a **CallChainHash**. This hash guarantees that the bundler cannot tamper with the execution order of the **SolverOperation**s. Any party can easily generate this hash by making a view call to the *getCallChainHash(SolverOperations[])* function. Note that infrastructure networks with programmable guarantees such as SUAVE will not require this as it can be handled trustlessly in-network. + +***Auctioneer Example***: +1. User connects to a DApp frontend and receives a session key from a FastLane x DApp backend. +2. User signs their UserOperation, which is propagated over the bloXroute BDN to solvers. +3. The frontend receives SolverOperations via the BDN. +4. After a set period of time, the frontend calls the *getCallChainHash()* view function via the User's RPC. +5. The frontend then uses the session key from step 1 to sign the **DAppOperation**, which includes the **CallChainHash**. +6. The frontend then propagates the DAppOperation over the BDN to bundlers. +7. Any bundler who tampers with the order of the SolverOperations will cause their transaction to revert, thereby blocking any gas reimbursement from Atlas. + +Note that input from the User is only required for step 2; all other steps have no impact on UX. + + ### Atlas Transaction Structure ![AtlasTransaction](./AtlasTransactionOverview.jpg) @@ -50,6 +68,10 @@ The DAppControl contract has the option to define functions that execute at the *These functions are executed by the Execution Environment via "delegatecall." +### Atlas Frontend / Infrastructure Flow + +![AtlasFlow](./AtlasFlow.jpeg) + ### Advantages: - Atlas Solvers have first access to any value created by the User Operation. This exclusive access supercedes that of any wallets, RPCs, relays, builders, validators, and sequencers. @@ -59,16 +81,16 @@ The DAppControl contract has the option to define functions that execute at the - Due to the unique nature of the Execution Environment - a smart account that Atlas creates to facilitate a trustless environment for Users, Solvers, and DApps - Users have an extra layer of protection against allowance-based exploits. -- DApp Governance has the option to subsidize a User's gas cost. Note that unlike traditional Account Abstraction protocols, Atlas empowers DApp Governance to subsidize the User's gas costs *conditionally* based on the *result* of the User's (or Solver's) execution. +- DApp Governance has the option to subsidize a User's gas cost. Note that unlike traditional Account Abstraction protocols, Atlas empowers DApp Governance to subsidize the User's gas costs *conditionally* based on the *result* of the User's (or Solver's) execution. We expect that most DApps will require Solvers to subsidize all gas costs not attributed to other Solvers. - By putting control of any User-created value in the hands of each DApp's Governance team, and by retaining the MEV before any RPCs or private relays see the transaction, Atlas has the potential to nullify the value of private orderflow, thereby acting as a counterforce to one of the strongest centralization risks in the Ethereum ecosystem. ### Disadvantages: -- Just as in the early days of Ethereum, Solvers do not benefit from "free reverts." If a Solver Operation fails, then the Solver still must pay their gas cost to the User. +- Just as in the early days of Ethereum, Solvers do not benefit from "free reverts." If a Solver Operation fails, then the Solver still must pay their gas cost to the Bundler. -- Atlas represents a less efficient use of block space than traditional, infrastructure-based MEV capture systems. This arises due to the checks and verifications that allow Atlas to function without relying on privacy guarantees from centralized, third-party infrastructure or off-chain agreements with permissioned builders. +- Atlas represents a less efficient use of block space than traditional, infrastructure-based MEV capture systems. This arises due to the checks and verifications that allow Atlas to function without relying on privacy guarantees from centralized, third-party infrastructure or off-chain agreements with permissioned builders. Note that this extra usage of gas will typically be handled by Solvers, and that if no Solver is willing to pay for the increased gas cost then the User can simply do a non-Atlas transaction. In other words, the extra gas cost will only be incurred when its cost is less than its benefit. ### Notes: -Note that the Bundler's backend may want to use a reputation system for solver bids in order to not take up too much space in the block. The further down the the solverOps[], the higher the reputation requirement for inclusion by the backend. This isnt necessarily required - it's not an economic issue - it's just that it's important to be a good member of the ecosystem and not waste too much precious blockspace by filling it with probabalistic solver txs that have a low success rate but a high profit-to-cost ratio. \ No newline at end of file +Note that the auctioneer (typically the frontend) may want to use a reputation system for solver bids in order to not take up too much space in the block. The further down the the solverOps[], the higher the reputation requirement for inclusion by the backend. This isnt necessarily required - it's not an economic issue - it's just that it's important to be a good member of the ecosystem and not waste too much precious blockspace by filling it with probabalistic solver txs that have a low success rate but a high profit-to-cost ratio. diff --git a/foundry.toml b/foundry.toml index b14e33c5..b7285cac 100644 --- a/foundry.toml +++ b/foundry.toml @@ -23,4 +23,14 @@ tab_width = 4 wrap_comments = true +[fmt] + bracket_spacing = true + int_types = "long" + line_length = 120 + multiline_func_header = "all" + number_underscore = "thousands" + quote_style = "double" + tab_width = 4 + wrap_comments = true + fs_permissions = [{ access = "read-write", path = "./"}] \ No newline at end of file diff --git a/script/base/deploy-base.s.sol b/script/base/deploy-base.s.sol index df3edb56..5dc7806b 100644 --- a/script/base/deploy-base.s.sol +++ b/script/base/deploy-base.s.sol @@ -17,6 +17,8 @@ import { TxBuilder } from "src/contracts/helpers/TxBuilder.sol"; import { Simulator } from "src/contracts/helpers/Simulator.sol"; import { SimpleRFQSolver } from "test/SwapIntent.t.sol"; +import { Utilities } from "src/contracts/helpers/Utilities.sol"; + contract DeployBaseScript is Script { using stdJson for string; @@ -33,6 +35,8 @@ contract DeployBaseScript is Script { TxBuilder public txBuilder; SimpleRFQSolver public rfqSolver; + Utilities public u; + function _getDeployChain() internal view returns (string memory) { // OPTIONS: LOCAL, SEPOLIA, MAINNET string memory deployChain = vm.envString("DEPLOY_TO"); diff --git a/script/deploy-solver.s.sol b/script/deploy-solver.s.sol index 07e3e4cb..985f8a4f 100644 --- a/script/deploy-solver.s.sol +++ b/script/deploy-solver.s.sol @@ -15,13 +15,17 @@ contract DeploySimpleRFQSolverScript is DeployBaseScript { uint256 deployerPrivateKey = vm.envUint("SOLVER1_PRIVATE_KEY"); address deployer = vm.addr(deployerPrivateKey); address atlasAddress = _getAddressFromDeploymentsJson("ATLAS"); + address wethAddress = u.getUsefulContractAddress(vm.envString("DEPLOY_TO"), "WETH"); console.log("Deployer address: \t\t\t\t", deployer); console.log("Using Atlas address: \t\t\t\t", atlasAddress); vm.startBroadcast(deployerPrivateKey); - rfqSolver = new SimpleRFQSolver(atlasAddress); + rfqSolver = new SimpleRFQSolver({ + weth: wethAddress, + atlas: atlasAddress + }); vm.stopBroadcast(); diff --git a/src/contracts/helpers/Utilities.sol b/src/contracts/helpers/Utilities.sol new file mode 100644 index 00000000..feeb5be8 --- /dev/null +++ b/src/contracts/helpers/Utilities.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import "forge-std/Script.sol"; +import "forge-std/Test.sol"; +import "forge-std/StdJson.sol"; + +contract Utilities is Script { + using stdJson for string; + + function getUsefulContractAddress(string memory chain, string memory key) public view returns (address) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/useful-addresses.json"); + string memory json = vm.readFile(path); + string memory fullKey = string.concat(".", chain, ".", key); + + address res = json.readAddress(fullKey); + if (res == address(0x0000000000000000000000000000000000000020)) { + revert(string.concat(fullKey, " not found in useful-addresses.json")); + } + return res; + } +} diff --git a/src/contracts/solver/SolverBase.sol b/src/contracts/solver/SolverBase.sol index 4b69c17b..dabfdbb8 100644 --- a/src/contracts/solver/SolverBase.sol +++ b/src/contracts/solver/SolverBase.sol @@ -16,13 +16,14 @@ interface IWETH9 { } contract SolverBase is Test { - address public constant WETH_ADDRESS = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + address public immutable WETH_ADDRESS; // TODO consider making these accessible (internal) for solvers which may want to use them address private immutable _owner; address private immutable _escrow; - constructor(address atlasEscrow, address owner) { + constructor(address weth, address atlasEscrow, address owner) { + WETH_ADDRESS = weth; _owner = owner; _escrow = atlasEscrow; } diff --git a/src/contracts/solver/src/TestSolver.sol b/src/contracts/solver/src/TestSolver.sol index 0b403de9..fed00135 100644 --- a/src/contracts/solver/src/TestSolver.sol +++ b/src/contracts/solver/src/TestSolver.sol @@ -7,5 +7,5 @@ import { SolverBase } from "../SolverBase.sol"; import { BlindBackrun } from "./BlindBackrun/BlindBackrun.sol"; contract Solver is SolverBase, BlindBackrun { - constructor(address atlasEscrow, address owner) SolverBase(atlasEscrow, owner) { } + constructor(address weth, address atlasEscrow, address owner) SolverBase(weth, atlasEscrow, owner) { } } diff --git a/test/Accounting.t.sol b/test/Accounting.t.sol index 9c8bf5fa..8b19d06c 100644 --- a/test/Accounting.t.sol +++ b/test/Accounting.t.sol @@ -64,7 +64,7 @@ contract AccountingTest is BaseTest { function testSolverBorrowRepaySuccessfully() public { // Solver deploys the RFQ solver contract (defined at bottom of this file) vm.startPrank(solverOneEOA); - HonestRFQSolver honestSolver = new HonestRFQSolver(address(atlas)); + HonestRFQSolver honestSolver = new HonestRFQSolver(WETH_ADDRESS, address(atlas)); vm.stopPrank(); SolverOperation[] memory solverOps = _setupBorrowRepayTestUsingBasicSwapIntent(address(honestSolver)); @@ -90,7 +90,7 @@ contract AccountingTest is BaseTest { // Solver deploys the RFQ solver contract (defined at bottom of this file) vm.startPrank(solverOneEOA); // TODO make evil solver - HonestRFQSolver evilSolver = new HonestRFQSolver(address(atlas)); + HonestRFQSolver evilSolver = new HonestRFQSolver(WETH_ADDRESS, address(atlas)); // atlas.deposit{value: gasCostCoverAmount}(solverOneEOA); vm.stopPrank(); @@ -219,7 +219,7 @@ contract AccountingTest is BaseTest { contract HonestRFQSolver is SolverBase { address public immutable ATLAS; - constructor(address atlas) SolverBase(atlas, msg.sender) { + constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { ATLAS = atlas; } @@ -250,7 +250,7 @@ contract HonestRFQSolver is SolverBase { contract EvilRFQSolver is HonestRFQSolver { address deployer; - constructor(address atlas) HonestRFQSolver(atlas) { + constructor(address weth, address atlas) HonestRFQSolver(weth, atlas) { deployer = msg.sender; } diff --git a/test/SwapIntent.t.sol b/test/SwapIntent.t.sol index 8273d1a5..0afee2d5 100644 --- a/test/SwapIntent.t.sol +++ b/test/SwapIntent.t.sol @@ -15,22 +15,6 @@ import { DAppOperation, DAppConfig } from "../src/contracts/types/DAppApprovalTy import { SwapIntentController, SwapIntent, Condition } from "../src/contracts/examples/intents-example/SwapIntent.sol"; import { SolverBase } from "../src/contracts/solver/SolverBase.sol"; -// QUESTIONS: - -// Refactor Ideas: -// 1. Lots of bitwise operations explicitly coded in contracts - could be a helper lib thats more readable -// 2. helper is currently a V2Helper and shared from BaseTest. Should only be in Uni V2 related tests -// 3. Need a more generic helper for BaseTest -// 4. Gonna be lots of StackTooDeep errors. Maybe need a way to elegantly deal with that in BaseTest -// 5. Change atlasSolverCall structure in SolverBase - maybe virtual fn to be overridden, which hooks for checks -// 6. Maybe emit error msg or some other better UX for error if !valid in metacall() - -// Doc Ideas: -// 1. Step by step instructions for building a metacall transaction (for internal testing, and integrating dApps) - -// To Understand Better: -// 1. The lock system (and look for any gas optimizations / ways to reduce lock actions) - interface IUniV2Router02 { function swapExactTokensForTokens( uint256 amountIn, @@ -84,7 +68,6 @@ contract SwapIntentTest is BaseTest { function testAtlasSwapIntentWithBasicRFQ() public { // Swap 10 WETH for 20 DAI - UserCondition userCondition = new UserCondition(); Condition[] memory conditions = new Condition[](2); @@ -109,7 +92,7 @@ contract SwapIntentTest is BaseTest { // Solver deploys the RFQ solver contract (defined at bottom of this file) vm.startPrank(solverOneEOA); - SimpleRFQSolver rfqSolver = new SimpleRFQSolver(address(atlas)); + SimpleRFQSolver rfqSolver = new SimpleRFQSolver(WETH_ADDRESS, address(atlas)); atlas.deposit{ value: 1e18 }(); vm.stopPrank(); @@ -230,7 +213,7 @@ contract SwapIntentTest is BaseTest { // Solver deploys the RFQ solver contract (defined at bottom of this file) vm.startPrank(solverOneEOA); - UniswapIntentSolver uniswapSolver = new UniswapIntentSolver(address(atlas)); + 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 }(); vm.stopPrank(); @@ -340,7 +323,7 @@ contract SwapIntentTest is BaseTest { // 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 atlas) SolverBase(atlas, msg.sender) { } + constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) { } function fulfillRFQ(SwapIntent calldata swapIntent, address executionEnvironment) public { require( @@ -368,7 +351,7 @@ contract SimpleRFQSolver is SolverBase { contract UniswapIntentSolver is SolverBase { IUniV2Router02 router = IUniV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); - constructor(address atlas) SolverBase(atlas, msg.sender) { } + 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 diff --git a/test/base/BaseTest.t.sol b/test/base/BaseTest.t.sol index f797faa1..4bfd8093 100644 --- a/test/base/BaseTest.t.sol +++ b/test/base/BaseTest.t.sol @@ -22,6 +22,8 @@ import { TestConstants } from "./TestConstants.sol"; import { V2Helper } from "../V2Helper.sol"; +import { Utilities } from "src/contracts/helpers/Utilities.sol"; + contract BaseTest is Test, TestConstants { address public me = address(this); @@ -57,6 +59,8 @@ contract BaseTest is Test, TestConstants { V2Helper public helper; + Utilities public u; + // Fork stuff ChainVars public chain = mainnet; uint256 public forkNetwork; @@ -126,7 +130,7 @@ contract BaseTest is Test, TestConstants { vm.startPrank(solverOneEOA); - solverOne = new Solver(escrow, solverOneEOA); + solverOne = new Solver(WETH_ADDRESS, escrow, solverOneEOA); atlas.deposit{ value: 1e18 }(); deal(TOKEN_ZERO, address(solverOne), 10e24); @@ -136,7 +140,7 @@ contract BaseTest is Test, TestConstants { vm.startPrank(solverTwoEOA); - solverTwo = new Solver(escrow, solverTwoEOA); + solverTwo = new Solver(WETH_ADDRESS, escrow, solverTwoEOA); atlas.deposit{ value: 1e18 }(); vm.stopPrank(); @@ -145,6 +149,7 @@ contract BaseTest is Test, TestConstants { deal(TOKEN_ONE, address(solverTwo), 10e24); helper = new V2Helper(address(control), address(atlas), address(atlasVerification)); + u = new Utilities(); deal(TOKEN_ZERO, address(atlas), 1); deal(TOKEN_ONE, address(atlas), 1); diff --git a/test/base/TestConstants.sol b/test/base/TestConstants.sol index 77491b68..84f8afd2 100644 --- a/test/base/TestConstants.sol +++ b/test/base/TestConstants.sol @@ -8,15 +8,22 @@ import { IUniswapV2Pair } from "../../src/contracts/examples/v2-example/interfac contract TestConstants { uint256 public constant BLOCK_START = 17_441_786; - // MAINNET - ChainVars public mainnet = ChainVars({ rpcUrlKey: "MAINNET_RPC_URL", forkBlock: BLOCK_START }); - // Structs struct ChainVars { string rpcUrlKey; uint256 forkBlock; + address weth; + address dai; } + // MAINNET + ChainVars public mainnet = ChainVars({ + rpcUrlKey: "MAINNET_RPC_URL", + forkBlock: BLOCK_START, + weth: address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), + dai: address(0x6B175474E89094C44Da98b954EedeAC495271d0F) + }); + // Constants address public constant FXS_ADDRESS = address(0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0); address public constant WETH_ADDRESS = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); diff --git a/useful-addresses.json b/useful-addresses.json new file mode 100644 index 00000000..ada949b3 --- /dev/null +++ b/useful-addresses.json @@ -0,0 +1,16 @@ +{ + "MAINNET": { + "WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "UNI": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "UNISWAP_V2_ROUTER": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" + }, + "SEPOLIA": { + "WETH": "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", + "DAI": "", + "USDC": "", + "UNI": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "UNISWAP_V2_ROUTER": "0x8f1dD60dBDb493DD940a44985AB43FB9901dcd2e" + } +} \ No newline at end of file