diff --git a/src/contracts/atlas/Atlas.sol b/src/contracts/atlas/Atlas.sol index 048a27e65..3352695d0 100644 --- a/src/contracts/atlas/Atlas.sol +++ b/src/contracts/atlas/Atlas.sol @@ -35,6 +35,8 @@ contract Atlas is Test, Factory { return; } + // TODO: Make sure all searcher nonces are incremented on fail. + // Check that the value of the tx is greater than or equal to the value specified // NOTE: a msg.value *higher* than user value could be used by the staging call. // There is a further check in the handler before the usercall to verify. @@ -49,7 +51,7 @@ contract Atlas is Test, Factory { gasMarker = gasleft(); // Get the execution environment - address environment = _prepEnvironment(protocolCall, keccak256(abi.encodePacked(userCall.to, userCall.data))); + address environment = _setExecutionEnvironment(protocolCall, userCall.from, verification.proof.controlCodeHash); console.log("contract creation gas cost", gasMarker - gasleft()); @@ -61,21 +63,18 @@ contract Atlas is Test, Factory { // Begin execution bytes32 callChainHashHead = _execute(protocolCall, userCall, searcherCalls, environment); + // Verify that the meta transactions were executed in the correct sequence require(callChainHashHead == verification.proof.callChainHash, "ERR-F05 InvalidCallChain"); + // Gas Refund to sender + _executeGasRefund(msg.sender); + // Release the lock _releaseEscrowLocks(); console.log("ex contract creation gas cost", gasMarker - gasleft()); } - function _prepEnvironment(ProtocolCall calldata protocolCall, bytes32 userCallHash) - internal - returns (address environment) - { - environment = _deployExecutionEnvironment(protocolCall, userCallHash); - } - function _execute( ProtocolCall calldata protocolCall, UserCall calldata userCall, @@ -85,8 +84,9 @@ contract Atlas is Test, Factory { // Build the CallChainProof. The penultimate hash will be used // to verify against the hash supplied by ProtocolControl CallChainProof memory proof = CallVerification.initializeProof(protocolCall, userCall); + bytes32 userCallHash = keccak256(abi.encodePacked(userCall.to, userCall.data)); - bytes memory stagingReturnData = _executeStagingCall(protocolCall, userCall, proof, environment); + bytes memory stagingReturnData = _executeStagingCall(protocolCall, userCall, environment); proof = proof.next(userCall.from, userCall.data); @@ -94,33 +94,41 @@ contract Atlas is Test, Factory { uint256 i; bool auctionAlreadyWon; + for (; i < searcherCalls.length;) { + proof = proof.next(searcherCalls[i].metaTx.from, searcherCalls[i].metaTx.data); - auctionAlreadyWon = auctionAlreadyWon - || _searcherExecutionIteration( - protocolCall, searcherCalls[i], proof, auctionAlreadyWon, environment - ); + // Only execute searcher meta tx if userCallHash matches + if (userCallHash == searcherCalls[i].metaTx.userCallHash) { + auctionAlreadyWon = auctionAlreadyWon + || _searcherExecutionIteration( + protocolCall, searcherCalls[i], auctionAlreadyWon, environment + ); + } + unchecked { ++i; } } - _executeUserRefund(userCall.from); - - _executeVerificationCall(protocolCall, proof, stagingReturnData, userReturnData, environment); + // If no searcher was successful, manually transition the lock + if (!auctionAlreadyWon) { + _notMadJustDisappointed(); + } + _executeVerificationCall(protocolCall, stagingReturnData, userReturnData, environment); + return proof.targetHash; } function _searcherExecutionIteration( ProtocolCall calldata protocolCall, SearcherCall calldata searcherCall, - CallChainProof memory proof, bool auctionAlreadyWon, address environment ) internal returns (bool) { - if (_executeSearcherCall(searcherCall, proof, auctionAlreadyWon, environment)) { + if (_executeSearcherCall(searcherCall, auctionAlreadyWon, environment)) { if (!auctionAlreadyWon) { auctionAlreadyWon = true; _executePayments(protocolCall, searcherCall.bids, environment); diff --git a/src/contracts/atlas/Escrow.sol b/src/contracts/atlas/Escrow.sol index 27d2ecf6b..472897146 100644 --- a/src/contracts/atlas/Escrow.sol +++ b/src/contracts/atlas/Escrow.sol @@ -17,14 +17,12 @@ import "../types/LockTypes.sol"; import "../types/VerificationTypes.sol"; import {EscrowBits} from "../libraries/EscrowBits.sol"; -import {CallChainProof} from "../libraries/CallVerification.sol"; import {CallVerification} from "../libraries/CallVerification.sol"; // import "forge-std/Test.sol"; contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper { using ECDSA for bytes32; - using CallVerification for CallChainProof; uint32 public immutable escrowDuration; @@ -68,10 +66,9 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper { function _executeStagingCall( ProtocolCall calldata protocolCall, UserCall calldata userCall, - CallChainProof memory proof, address environment ) internal stagingLock(protocolCall, environment) returns (bytes memory stagingReturnData) { - stagingReturnData = IExecutionEnvironment(environment).stagingWrapper{value: msg.value}(proof, userCall); + stagingReturnData = IExecutionEnvironment(environment).stagingWrapper{value: msg.value}(userCall); } function _executeUserCall(UserCall calldata userCall, address environment) @@ -84,7 +81,6 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper { function _executeSearcherCall( SearcherCall calldata searcherCall, - CallChainProof memory proof, bool isAuctionAlreadyComplete, address environment ) internal returns (bool) { @@ -104,7 +100,7 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper { _openSearcherLock(searcherCall.metaTx.to, environment); // Execute the searcher call - (outcome, escrowSurplus) = _searcherCallWrapper(searcherCall, proof, gasLimit, environment); + (outcome, escrowSurplus) = _searcherCallWrapper(searcherCall, gasLimit, environment); unchecked { searcherEscrow.total += uint128(escrowSurplus); @@ -120,7 +116,6 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper { result |= 1 << uint256(SearcherOutcome.ExecutionCompleted); } - uint256 gasRebate; // TODO: can reuse gasWaterMark here for gas efficiency if it really matters // Update the searcher's escrow balances @@ -175,17 +170,14 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper { function _executeVerificationCall( ProtocolCall calldata protocolCall, - CallChainProof memory proof, bytes memory stagingReturnData, bytes memory userReturnData, address environment ) internal verificationLock(protocolCall.callConfig, environment) { - proof = proof.addVerificationCallProof(protocolCall.to, stagingReturnData, userReturnData); - - IExecutionEnvironment(environment).verificationWrapper(proof, stagingReturnData, userReturnData); + IExecutionEnvironment(environment).verificationWrapper(stagingReturnData, userReturnData); } - function _executeUserRefund(address userCallFrom) internal { + function _executeGasRefund(address gasPayor) internal { uint256 gasRebate = uint256(_escrowKey.gasRefund) * tx.gasprice; /* @@ -196,7 +188,7 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper { ); */ - SafeTransferLib.safeTransferETH(userCallFrom, gasRebate); + SafeTransferLib.safeTransferETH(gasPayor, gasRebate); } function _update( @@ -267,8 +259,9 @@ contract Escrow is ProtocolVerifier, SafetyLocks, SearcherWrapper { metaTx.value, metaTx.gas, metaTx.nonce, - metaTx.userCallHash, metaTx.maxFeePerGas, + metaTx.userCallHash, + metaTx.controlCodeHash, metaTx.bidsHash, keccak256(metaTx.data) ) diff --git a/src/contracts/atlas/ExecutionEnvironment.sol b/src/contracts/atlas/ExecutionEnvironment.sol index 3b8099e8d..217ca0741 100644 --- a/src/contracts/atlas/ExecutionEnvironment.sol +++ b/src/contracts/atlas/ExecutionEnvironment.sol @@ -8,7 +8,6 @@ import {IProtocolControl} from "../interfaces/IProtocolControl.sol"; import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; import {UserCall, ProtocolCall, SearcherCall, BidData} from "../types/CallTypes.sol"; -import {CallChainProof} from "../types/VerificationTypes.sol"; import {CallVerification} from "../libraries/CallVerification.sol"; import {CallBits} from "../libraries/CallBits.sol"; @@ -24,7 +23,6 @@ import { } from "./Emissions.sol"; contract ExecutionEnvironment is Test { - using CallVerification for CallChainProof; using CallBits for uint16; address public immutable atlas; @@ -34,9 +32,9 @@ contract ExecutionEnvironment is Test { } // MIMIC INTERACTION FUNCTIONS - function _userCallHash() internal pure returns (bytes32 userCallHash) { + function _controlCodeHash() internal pure returns (bytes32 controlCodeHash) { assembly { - userCallHash := calldataload(sub(calldatasize(), 32)) + controlCodeHash := calldataload(sub(calldatasize(), 32)) } } @@ -61,39 +59,36 @@ contract ExecutionEnvironment is Test { ////////////////////////////////// /// CORE CALL FUNCTIONS /// ////////////////////////////////// - function stagingWrapper(CallChainProof calldata proof, UserCall calldata userCall) + function stagingWrapper(UserCall calldata userCall) external - returns (bytes memory stagingData) + returns (bytes memory) { // msg.sender = atlas // address(this) = ExecutionEnvironment address control = _control(); - uint16 config = _config(); require(msg.sender == atlas && userCall.from == _user(), "ERR-CE00 InvalidSenderStaging"); - require(keccak256(abi.encodePacked(userCall.to, userCall.data)) == _userCallHash(), "ERR-CD01 CalldataInvalid"); - stagingData = abi.encodeWithSelector( + bytes memory stagingData = abi.encodeWithSelector( IProtocolControl.stageCall.selector, userCall.to, userCall.from, bytes4(userCall.data), userCall.data[4:] ); - // Verify the proof so that the callee knows this isn't happening out of sequence. - require(proof.prove(control, stagingData), "ERR-P01 ProofInvalid"); + stagingData = abi.encodePacked( + stagingData, + _user(), + _control(), + _config(), + _controlCodeHash() + ); bool success; - if (config.needsDelegateStaging()) { - (success, stagingData) = control.delegatecall(stagingData); - require(success, "ERR-EC02 DelegateRevert"); - } else { - (success, stagingData) = control.staticcall(stagingData); - require(success, "ERR-EC03 StaticRevert"); - } + (success, stagingData) = control.delegatecall(stagingData); + require(success, "ERR-EC02 DelegateRevert"); + + return stagingData; - if (!config.needsVerificationCall()) { - delete stagingData; - } } function userWrapper(UserCall calldata userCall) external payable returns (bytes memory userData) { @@ -104,38 +99,46 @@ contract ExecutionEnvironment is Test { uint16 config = _config(); require(msg.sender == atlas && userCall.from == user, "ERR-CE00 InvalidSenderUser"); - require(keccak256(abi.encodePacked(userCall.to, userCall.data)) == _userCallHash(), "ERR-CD02 CalldataInvalid"); require(address(this).balance >= userCall.value, "ERR-CE01 ValueExceedsBalance"); bool success; // regular user call - executed at regular destination and not performed locally if (!config.needsLocalUser()) { - (success, userData) = userCall.to.call{value: userCall.value}(userCall.data); + (success, userData) = userCall.to.call{value: userCall.value}( + abi.encodePacked( + userCall.data, + user, + _control(), + config, + _controlCodeHash() + ) + ); require(success, "ERR-EC04a CallRevert"); + } else { if (config.needsDelegateUser()) { userData = abi.encodeWithSelector( IProtocolControl.userLocalCall.selector, userCall.to, userCall.value, userCall.data ); + + userData = abi.encodePacked( + userData, + user, + _control(), + config, + _controlCodeHash() + ); + (success, userData) = _control().delegatecall(userData); require(success, "ERR-EC02 DelegateRevert"); } else { revert("ERR-P02 UserCallStatic"); } } - - if (!config.needsVerificationCall()) { - delete userData; - } - - // NOTE: selfdestruct will continue to work post EIP-6780 when it is triggered - // in the same transaction as contract creation, which is what we do here. - // selfdestruct(payable(user)); // NOTE: Must comment out for forge tests } function verificationWrapper( - CallChainProof calldata proof, bytes calldata stagingReturnData, bytes calldata userReturnData ) external { @@ -146,22 +149,21 @@ contract ExecutionEnvironment is Test { bytes memory data = abi.encodeWithSelector(IProtocolControl.verificationCall.selector, stagingReturnData, userReturnData); - // Verify the proof so that the callee knows this isn't happening out of sequence. - require(proof.prove(_control(), data), "ERR-P01 ProofInvalid"); + data = abi.encodePacked( + data, + _user(), + _control(), + _config(), + _controlCodeHash() + ); - if (_config().needsDelegateVerification()) { - (bool success, bytes memory returnData) = _control().delegatecall(data); - require(success, "ERR-EC02 DelegateRevert"); - require(abi.decode(returnData, (bool)), "ERR-EC03a DelegateUnsuccessful"); - } else { - (bool success, bytes memory returnData) = _control().staticcall(data); - require(success, "ERR-EC03 StaticRevert"); - require(abi.decode(returnData, (bool)), "ERR-EC03b DelegateUnsuccessful"); - } + (bool success, bytes memory returnData) = _control().delegatecall(data); + + require(success, "ERR-EC02 DelegateRevert"); + require(abi.decode(returnData, (bool)), "ERR-EC03a DelegateUnsuccessful"); } function searcherMetaTryCatch( - CallChainProof calldata proof, uint256 gasLimit, uint256 escrowBalance, SearcherCall calldata searcherCall @@ -192,15 +194,8 @@ contract ExecutionEnvironment is Test { // SEARCHER SAFETY CHECKS // //////////////////////////// - // Verify that the searcher's view of the user's calldata hasn't been altered - // NOTE: Although this check may seem redundant since the user's calldata is in the - // searcher hash chain as verified below, remember that the protocol submits the - // full hash chain, which user verifies. This check therefore allows the searcher - // not to have to worry about user+protocol collaboration to exploit the searcher. - require(searcherCall.metaTx.userCallHash == _userCallHash(), ALTERED_USER_HASH); - - // Verify that the searcher's calldata is unaltered and being executed in the correct order - proof.proveCD(searcherCall.metaTx.from, searcherCall.metaTx.data); + // Verify that the ProtocolControl contract matches the searcher's expectations + require(searcherCall.metaTx.controlCodeHash == _controlCodeHash(), ALTERED_USER_HASH); // Execute the searcher call. (bool success,) = ISearcherContract(searcherCall.metaTx.to).metaFlashCall{ @@ -282,17 +277,19 @@ contract ExecutionEnvironment is Test { } } - if (_config().needsDelegateAllocating()) { - (bool success,) = _control().delegatecall( - abi.encodeWithSelector(IProtocolControl.allocatingCall.selector, abi.encode(totalEtherReward, bids)) - ); - require(success, "ERR-EC02 DelegateRevert"); - } else { - (bool success,) = _control().call{value: totalEtherReward}( - abi.encodeWithSelector(IProtocolControl.allocatingCall.selector, abi.encode(totalEtherReward, bids)) - ); - require(success, "ERR-EC04b CallRevert"); - } + bytes memory allocateData = abi.encodeWithSelector(IProtocolControl.allocatingCall.selector, abi.encode(totalEtherReward, bids)); + + allocateData = abi.encodePacked( + allocateData, + _user(), + _control(), + _config(), + _controlCodeHash() + ); + + (bool success,) = _control().delegatecall(allocateData); + require(success, "ERR-EC02 DelegateRevert"); + } /////////////////////////////////////// diff --git a/src/contracts/atlas/Factory.sol b/src/contracts/atlas/Factory.sol index 427b79ac3..84925c23c 100644 --- a/src/contracts/atlas/Factory.sol +++ b/src/contracts/atlas/Factory.sol @@ -7,12 +7,14 @@ import {IExecutionEnvironment} from "../interfaces/IExecutionEnvironment.sol"; import {Escrow} from "./Escrow.sol"; import {Mimic} from "./Mimic.sol"; import {ExecutionEnvironment} from "./ExecutionEnvironment.sol"; +import {TokenTransfers} from "./TokenTransfers.sol"; import "../types/CallTypes.sol"; +import {EscrowKey} from "../types/LockTypes.sol"; import "forge-std/Test.sol"; -contract Factory is Test, Escrow { +contract Factory is Test, Escrow, TokenTransfers { //address immutable public atlas; bytes32 public immutable salt; address public immutable execution; @@ -59,24 +61,25 @@ contract Factory is Test, Escrow { view returns (address executionEnvironment) { - bytes32 userCallHash = keccak256(abi.encodePacked(userCall.to, userCall.data)); - executionEnvironment = _getExecutionEnvironment(userCall.from, userCallHash, protocolControl); + executionEnvironment = _getExecutionEnvironment(userCall.from, protocolControl.codehash, protocolControl); } - function _getExecutionEnvironment(address user, bytes32 userCallHash, address protocolControl) + function _getExecutionEnvironment(address user, bytes32 controlCodeHash, address protocolControl) internal view returns (address executionEnvironment) { ProtocolCall memory protocolCall = IProtocolControl(protocolControl).getProtocolCall(); - return _getExecutionEnvironmentCustom(user, userCallHash, protocolCall); + + executionEnvironment = _getExecutionEnvironmentCustom(user, controlCodeHash, protocolCall.to, protocolCall.callConfig); } // NOTE: This func is used to generate the address of user ExecutionEnvironments that have // been deprecated due to ProtocolControl changes of callConfig. - function _getExecutionEnvironmentCustom(address user, bytes32 userCallHash, ProtocolCall memory protocolCall) + function _getExecutionEnvironmentCustom(address user, bytes32 controlCodeHash, address protocolControl, uint16 callConfig) internal view + override returns (address environment) { environment = address( @@ -90,7 +93,7 @@ contract Factory is Test, Escrow { keccak256( abi.encodePacked( _getMimicCreationCode( - protocolCall.to, protocolCall.callConfig, execution, user, userCallHash + protocolControl, callConfig, execution, user, controlCodeHash ) ) ) @@ -101,16 +104,33 @@ contract Factory is Test, Escrow { ); } - function _deployExecutionEnvironment(ProtocolCall calldata protocolCall, bytes32 userCallHash) + function _setExecutionEnvironment(ProtocolCall calldata protocolCall, address user, bytes32 controlCodeHash) internal returns (address environment) { bytes memory creationCode = - _getMimicCreationCode(protocolCall.to, protocolCall.callConfig, execution, msg.sender, userCallHash); + _getMimicCreationCode(protocolCall.to, protocolCall.callConfig, execution, user, controlCodeHash); - bytes32 memSalt = salt; - assembly { - environment := create2(0, add(creationCode, 32), mload(creationCode), memSalt) + environment = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(abi.encodePacked(creationCode)) + ) + ) + ) + ) + ); + + if (environment.codehash == bytes32(0)) { + bytes32 memSalt = salt; + assembly { + environment := create2(0, add(creationCode, 32), mload(creationCode), memSalt) + } } } @@ -127,7 +147,7 @@ contract Factory is Test, Escrow { uint16 callConfig, address executionLib, address user, - bytes32 userCallHash + bytes32 controlCodeHash ) internal pure returns (bytes memory creationCode) { // NOTE: Changing compiler settings or solidity versions can break this. creationCode = type(Mimic).creationCode; @@ -138,7 +158,15 @@ contract Factory is Test, Escrow { add(creationCode, 152), add(shl(96, protocolControl), add(add(shl(88, 0x61), shl(72, callConfig)), 0x7f0000000000000000)) ) - mstore(add(creationCode, 176), userCallHash) + mstore(add(creationCode, 176), controlCodeHash) } } + + function _getLockState() internal view override returns (EscrowKey memory) { + return _escrowKey; + } + + function getLockState() external view returns (EscrowKey memory) { + return _escrowKey; + } } diff --git a/src/contracts/atlas/Mimic.sol b/src/contracts/atlas/Mimic.sol index b6c09f5f6..f85d6a30f 100644 --- a/src/contracts/atlas/Mimic.sol +++ b/src/contracts/atlas/Mimic.sol @@ -7,7 +7,7 @@ contract Mimic { 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB is standin for the user's EOA address 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC is standin for the protocol control address 0x2222 is standin for the call configuration - + 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee is the protocol control contract's .codehash These values are adjusted by the factory to match the appropriate values for the intended user/control/config. This happens during contract creation. @@ -31,7 +31,7 @@ contract Mimic { 0x7f0000000000000000 ) )) - mstore(add(creationCode, 176), userCallHash) + mstore(add(creationCode, 176), controlCodeHash) } */ diff --git a/src/contracts/atlas/ProtocolVerification.sol b/src/contracts/atlas/ProtocolVerification.sol index 12d0d9631..c3328ec89 100644 --- a/src/contracts/atlas/ProtocolVerification.sol +++ b/src/contracts/atlas/ProtocolVerification.sol @@ -23,7 +23,7 @@ contract ProtocolVerifier is EIP712, ProtocolIntegration { using CallBits for uint16; bytes32 public constant PROTOCOL_TYPE_HASH = keccak256( - "ProtocolProof(address from,address to,uint256 nonce,uint256 deadline,bytes32 userCallHash,bytes32 callChainHash)" + "ProtocolProof(address from,address to,uint256 nonce,uint256 deadline,bytes32 userCallHash,bytes32 callChainHash,bytes32 controlCodeHash)" ); constructor() EIP712("ProtoCallHandler", "0.0.1") {} @@ -79,7 +79,14 @@ contract ProtocolVerifier is EIP712, ProtocolIntegration { return (false); } - // if the protocol indicated that they only accept sequenced nonces + // Verify that ProtocolControl hasn't been updated. + // NOTE: Performing this check here allows the searchers' checks + // to be against the verification proof's controlCodeHash to save gas. + if (controlCodeHash != verification.proof.controlCodeHash) { + return (false); + } + + // If the protocol indicated that they only accept sequenced nonces // (IE for FCFS execution), check and make sure the order is correct // NOTE: allowing only sequenced nonces could create a scenario in // which builders or validators may be able to profit via censorship. @@ -119,7 +126,8 @@ contract ProtocolVerifier is EIP712, ProtocolIntegration { proof.nonce, proof.deadline, proof.userCallHash, - proof.callChainHash + proof.callChainHash, + proof.controlCodeHash ) ); } diff --git a/src/contracts/atlas/SafetyLocks.sol b/src/contracts/atlas/SafetyLocks.sol index 8312ba810..0914c814b 100644 --- a/src/contracts/atlas/SafetyLocks.sol +++ b/src/contracts/atlas/SafetyLocks.sol @@ -10,7 +10,9 @@ import {ProtocolCall, UserCall} from "../types/CallTypes.sol"; import "../types/LockTypes.sol"; import "../types/EscrowTypes.sol"; -contract SafetyLocks { +import "forge-std/Test.sol"; + +contract SafetyLocks is Test { using SafetyBits for EscrowKey; using CallBits for uint16; @@ -166,6 +168,11 @@ contract SafetyLocks { } } + function _notMadJustDisappointed() internal { + EscrowKey memory escrowKey = _escrowKey; + _escrowKey = escrowKey.setAllSearchersFailed(); + } + ////////////////////////////////// //////////// GETTERS /////////// ////////////////////////////////// @@ -178,8 +185,4 @@ contract SafetyLocks { function confirmSafetyCallback() external view returns (bool) { return _escrowKey.confirmSearcherLock(msg.sender); } - - function getLockState() external view returns (EscrowKey memory) { - return _escrowKey; - } } diff --git a/src/contracts/atlas/SearcherWrapper.sol b/src/contracts/atlas/SearcherWrapper.sol index 63a717cc2..fc06dcd80 100644 --- a/src/contracts/atlas/SearcherWrapper.sol +++ b/src/contracts/atlas/SearcherWrapper.sol @@ -15,7 +15,6 @@ import {SearcherOutcome} from "../types/EscrowTypes.sol"; contract SearcherWrapper is FastLaneErrorsEvents { function _searcherCallWrapper( SearcherCall calldata searcherCall, - CallChainProof memory proof, uint256 gasLimit, address environment ) internal returns (SearcherOutcome, uint256) { @@ -27,7 +26,7 @@ contract SearcherWrapper is FastLaneErrorsEvents { // Call the execution environment try IExecutionEnvironment(environment).searcherMetaTryCatch{value: searcherCall.metaTx.value}( - proof, gasLimit, currentBalance, searcherCall + gasLimit, currentBalance, searcherCall ) { return (SearcherOutcome.Success, address(this).balance - currentBalance); } catch Error(string memory err) { diff --git a/src/contracts/atlas/TokenTransfers.sol b/src/contracts/atlas/TokenTransfers.sol new file mode 100644 index 000000000..684812662 --- /dev/null +++ b/src/contracts/atlas/TokenTransfers.sol @@ -0,0 +1,75 @@ +//SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.16; + +import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; + +import "../types/LockTypes.sol"; +import {ProtocolCall} from "../types/CallTypes.sol"; + +abstract contract TokenTransfers { + using SafeTransferLib for ERC20; + + function transferUserERC20( + address token, + address destination, + uint256 amount, + address user, + address protocolControl, + uint16 callConfig + ) external { + // Verify that the caller is legitimate + require(msg.sender == _getExecutionEnvironmentCustom(user, protocolControl.codehash, protocolControl, callConfig), "ERR-T001 ProtocolTransfer"); + + // Verify that the protocol is in control of the ExecutionEnvironment + require(_getLockState().lockState & _SAFE_USER_TRANSFER != 0, "ERR-T002 ProtocolTransfer"); + + // Transfer token + ERC20(token).safeTransferFrom(user, destination, amount); + } + + function transferProtocolERC20( + address token, + address destination, + uint256 amount, + address user, + address protocolControl, + uint16 callConfig + ) external { + // Verify that the caller is legitimate + require(msg.sender == _getExecutionEnvironmentCustom(user, protocolControl.codehash, protocolControl, callConfig), "ERR-T003 ProtocolTransfer"); + + // Verify that the protocol is in control of the ExecutionEnvironment + require(_getLockState().lockState & _SAFE_PROTOCOL_TRANSFER != 0, "ERR-T004 ProtocolTransfer"); + + // Transfer token + ERC20(token).safeTransferFrom(protocolControl, destination, amount); + } + + uint16 internal constant _EXECUTION_PHASE_OFFSET = uint16(type(BaseLock).max); + uint16 internal constant _SAFETY_LEVEL_OFFSET = uint16(type(BaseLock).max) + uint16(type(ExecutionPhase).max); + + uint16 internal constant _SAFE_USER_TRANSFER = uint16( + 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.Staging)) | + 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.UserCall)) | + 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.Verification)) + ); + + uint16 internal constant _SAFE_PROTOCOL_TRANSFER = uint16( + 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.Staging)) | + 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.HandlingPayments)) | + 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.UserRefund)) | + 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.Verification)) + ); + + function _isSafeUserTransfer() internal view returns (bool) { + return _getLockState().lockState & _SAFE_USER_TRANSFER != 0; + } + + function _getExecutionEnvironmentCustom(address user, bytes32 controlCodeHash, address protocolControl, uint16 callConfig) + internal + view + virtual + returns (address environment); + + function _getLockState() internal view virtual returns (EscrowKey memory); +} \ No newline at end of file diff --git a/src/contracts/interfaces/IExecutionEnvironment.sol b/src/contracts/interfaces/IExecutionEnvironment.sol index 8111c42b3..22697b190 100644 --- a/src/contracts/interfaces/IExecutionEnvironment.sol +++ b/src/contracts/interfaces/IExecutionEnvironment.sol @@ -2,10 +2,9 @@ pragma solidity ^0.8.16; import "../types/CallTypes.sol"; -import {CallChainProof} from "../types/VerificationTypes.sol"; interface IExecutionEnvironment { - function stagingWrapper(CallChainProof calldata proof, UserCall calldata userCall) + function stagingWrapper(UserCall calldata userCall) external payable returns (bytes memory stagingData); @@ -13,13 +12,11 @@ interface IExecutionEnvironment { function userWrapper(UserCall calldata userCall) external payable returns (bytes memory userReturnData); function verificationWrapper( - CallChainProof calldata proof, bytes calldata stagingReturnData, bytes calldata userReturnData ) external payable; function searcherMetaTryCatch( - CallChainProof calldata proof, uint256 gasLimit, uint256 escrowBalance, SearcherCall calldata searcherCall diff --git a/src/contracts/interfaces/ITokenTransfers.sol b/src/contracts/interfaces/ITokenTransfers.sol new file mode 100644 index 000000000..eb33ac72e --- /dev/null +++ b/src/contracts/interfaces/ITokenTransfers.sol @@ -0,0 +1,25 @@ +//SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.16; + +import "../types/CallTypes.sol"; +import "../types/VerificationTypes.sol"; + +interface ITokenTransfers { + function transferUserERC20( + address token, + address destination, + uint256 amount, + address user, + address protocolControl, + uint16 callConfig + ) external; + + function transferProtocolERC20( + address token, + address destination, + uint256 amount, + address user, + address protocolControl, + uint16 callConfig + ) external; +} diff --git a/src/contracts/libraries/SafetyBits.sol b/src/contracts/libraries/SafetyBits.sol index 798db7404..ba8ef9bef 100644 --- a/src/contracts/libraries/SafetyBits.sol +++ b/src/contracts/libraries/SafetyBits.sol @@ -62,6 +62,12 @@ library SafetyBits { | 1 << (_SAFETY_LEVEL_OFFSET + uint16(SearcherSafety.Unset)) ); + uint16 internal constant _NO_SEARCHER_SUCCESS = uint16( + 1 << uint16(BaseLock.Active) | + 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.Verification)) | + 1 << (_SAFETY_LEVEL_OFFSET + uint16(SearcherSafety.Unset)) + ); + uint16 internal constant _ACTIVE_X_REFUND_X_UNSET = uint16( 1 << uint16(BaseLock.Pending) | 1 << (_EXECUTION_PHASE_OFFSET + uint16(ExecutionPhase.UserRefund)) | 1 << (_SAFETY_LEVEL_OFFSET + uint16(SearcherSafety.Unset)) @@ -110,6 +116,17 @@ library SafetyBits { return self; } + function setAllSearchersFailed(EscrowKey memory self) + internal + pure + returns (EscrowKey memory) + { + self.lockState = _NO_SEARCHER_SUCCESS; + self.approvedCaller = address(0); + self.callIndex = self.callMax - 1; + return self; + } + function isValidVerificationLock(EscrowKey memory self, address caller) internal pure returns (bool) { // CASE: Previous searcher was successful if ((self.lockState == _LOCK_PAYMENTS)) { @@ -118,7 +135,13 @@ library SafetyBits { && (self.callIndex < self.callMax) ); - // CASE: No searchers were successful + // CASE: No searchers were successful + } else if (self.lockState == _NO_SEARCHER_SUCCESS) { + return ( + (!self.makingPayments) && (!self.paymentsComplete) && + (self.callIndex == self.callMax - 1) && (self.approvedCaller == address(0)) + ); + } else { return ( (self.lockState == _LOCKED_X_SEARCHERS_X_REQUESTED) && (caller != address(0)) @@ -127,19 +150,6 @@ library SafetyBits { } } - function turnRefundLock(EscrowKey memory self, address approvedCaller) internal pure returns (EscrowKey memory) { - self.lockState = _ACTIVE_X_VERIFICATION_X_UNSET; - self.approvedCaller = approvedCaller; - return self; - } - - function isValidRefundLock(EscrowKey memory self, address caller) internal pure returns (bool) { - return ( - (self.lockState == _ACTIVE_X_REFUND_X_UNSET) && (self.approvedCaller == caller) - && (self.callIndex == self.callMax - 2) - ); - } - function turnPaymentsLockSearcher(EscrowKey memory self, address approvedCaller) internal pure diff --git a/src/contracts/protocol/ExecutionBase.sol b/src/contracts/protocol/ExecutionBase.sol new file mode 100644 index 000000000..d609458f9 --- /dev/null +++ b/src/contracts/protocol/ExecutionBase.sol @@ -0,0 +1,55 @@ +//SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.16; + +import {ITokenTransfers} from "../interfaces/ITokenTransfers.sol"; + +contract ExecutionBase { + + // These functions only work inside of the ExecutionEnvironment (mimic) + // via delegatecall, but can be added to ProtocolControl as funcs that + // can be used during ProtocolControl's delegated funcs + + function _controlCodeHash() internal pure returns (bytes32 controlCodeHash) { + assembly { + controlCodeHash := calldataload(sub(calldatasize(), 32)) + } + } + + function _config() internal pure returns (uint16 config) { + assembly { + config := shr(240, calldataload(sub(calldatasize(), 34))) + } + } + + function _control() internal pure returns (address control) { + assembly { + control := shr(96, calldataload(sub(calldatasize(), 54))) + } + } + + function _user() internal pure returns (address user) { + assembly { + user := shr(96, calldataload(sub(calldatasize(), 74))) + } + } + + function _transferUserERC20( + address token, + address destination, + uint256 amount + ) internal { + ITokenTransfers(msg.sender).transferUserERC20( + token, destination, amount, _user(), _control(), _config() + ); + } + + function _transferProtocolERC20( + address token, + address destination, + uint256 amount + ) internal { + ITokenTransfers(msg.sender).transferProtocolERC20( + token, destination, amount, _user(), _control(), _config() + ); + } +} \ No newline at end of file diff --git a/src/contracts/protocol/GovernanceControl.sol b/src/contracts/protocol/GovernanceControl.sol index ddbe015a3..9244ca967 100644 --- a/src/contracts/protocol/GovernanceControl.sol +++ b/src/contracts/protocol/GovernanceControl.sol @@ -21,7 +21,7 @@ abstract contract GovernanceControl { // bytes calldata userCallData // - // _stageDelegateCall + // _stageCall // Details: // staging/delegate = // Inputs: User's calldata @@ -31,24 +31,11 @@ abstract contract GovernanceControl { // // Protocol exposure: Trustless // User exposure: Trustless - function _stageDelegateCall(address to, address from, bytes4 userSelector, bytes calldata userData) + function _stageCall(address to, address from, bytes4 userSelector, bytes calldata userData) internal virtual returns (bytes memory stagingData); - // _stageStaticCall - // Details: - // staging/static = - // Inputs: User's calldata - // Function: Executing the function set by ProtocolControl - // Container: Inside of the ProtocolControl contract - // Access: With storage access (read only) to the ProtocolControl - // - // Protocol exposure: Trustless - // User exposure: Trustless - function _stageStaticCall(address, address, bytes4, bytes calldata) internal view virtual returns (bytes memory) { - revert(_NOT_IMPLEMENTED); - } ///////////////////////////////////////////////////////// // USER // @@ -98,6 +85,22 @@ abstract contract GovernanceControl { revert(_NOT_IMPLEMENTED); } + ///////////////////////////////////////////////////////// + // MEV ALLOCATION // + ///////////////////////////////////////////////////////// + // + // _allocatingCall + // Details: + // allocate/delegate = + // Inputs: MEV Profits (ERC20 balances) + // Function: Executing the function set by ProtocolControl / MEVAllocator + // Container: Inside of the FastLane ExecutionEnvironment + // Access: With storage access (read + write) only to the ExecutionEnvironment + // + // Protocol exposure: Trustless + // User exposure: Trustless + function _allocatingCall(bytes calldata data) internal virtual; + ///////////////////////////////////////////////////////// // VERIFICATION // ///////////////////////////////////////////////////////// @@ -108,25 +111,14 @@ abstract contract GovernanceControl { // bytes memory userReturnData // - // _verificationDelegateCall + // _verificationCall // Details: // verification/delegatecall = // Inputs: User's return data + staging call's returnData // Function: Executing the function set by ProtocolControl // Container: Inside of the FastLane ExecutionEnvironment // Access: Storage access (read+write) to the ExecutionEnvironment contract - function _verificationDelegateCall(bytes calldata) internal virtual returns (bool) { - revert(_NOT_IMPLEMENTED); - } - - // _verificationStaticCall - // Details: - // verification/delegatecall = - // Inputs: User's return data + staging call's returnData - // Function: Executing the function set by ProtocolControl - // Container: Inside of the ProtocolControl contract - // Access: Storage access (read only) to the ProtocolControl contract - function _verificationStaticCall(bytes calldata) internal view virtual returns (bool) { + function _verificationCall(bytes calldata) internal virtual returns (bool) { revert(_NOT_IMPLEMENTED); } diff --git a/src/contracts/protocol/MEVAllocator.sol b/src/contracts/protocol/MEVAllocator.sol deleted file mode 100644 index c14ee18dc..000000000 --- a/src/contracts/protocol/MEVAllocator.sol +++ /dev/null @@ -1,112 +0,0 @@ -//SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.16; - -import {IProtocolControl} from "../interfaces/IProtocolControl.sol"; - -import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; - -import "../types/CallTypes.sol"; - -abstract contract MEVAllocator { - // Virtual functions to be overridden by participating protocol governance - // (not FastLane) prior to deploying contract. Note that protocol governance - // will "own" this contract but that it should be immutable. - - ///////////////////////////////////////////////////////// - // MEV ALLOCATION // - ///////////////////////////////////////////////////////// - // - // MEV Allocation: - // Data should be decoded as: - // - // uint256 totalEtherReward, - // BidData[] memory bids - // - - // _allocateDelegateCall - // Details: - // allocate/delegate = - // Inputs: MEV Profits (ERC20 balances) - // Function: Executing the function set by ProtocolControl / MEVAllocator - // Container: Inside of the FastLane ExecutionEnvironment - // Access: With storage access (read + write) only to the ExecutionEnvironment - // - // Protocol exposure: Trustless - // User exposure: Trustless - function _allocatingDelegateCall(bytes calldata data) internal virtual; - - // _allocateStandardCall - // Details: - // allocate/standard call = - // Inputs: MEV Profits (ERC20 balances) and payeeData sourced by protocol frontend - // Function: Executing the function set by ProtocolControl / MEVAllocator - // Container: Inside of the ProtocolControl contract - // Access: With storage access (read + write) to the ProtocolControl contract - // - // NOTE: Currently disallowed due to all MEV rewards being held inn ExecutionEnvironment and - // because changing that could disrupt trustlessness. - // TODO: More research to find a way to do this trustlessly. - function _allocatingStandardCall( - bytes calldata // data - ) - // ) internal virtual; - internal - pure - { - require(true == false, "ERR-MEVA01 AllocateStandardDisallowed"); - } -} - -contract AllocationExample { - address public immutable control; - - constructor() { - control = address(this); - } - - function _exampleAllocateMEV(BidData[] memory bids) internal { - PaymentData memory pmtData; - - address tokenAddress; - uint256 payment; - uint256 bidAmount; - uint256 remainder; - - uint256 i; - uint256 k; - - bytes memory nullBytes; - - PayeeData[] memory payeeData = IProtocolControl(control).getPayeeData(nullBytes); - - for (; i < bids.length;) { - tokenAddress = bids[i].token; - bidAmount = bids[i].bidAmount; - remainder = bidAmount; - - for (; k < payeeData[i].payments.length;) { - pmtData = payeeData[i].payments[k]; - - payment = bidAmount * pmtData.payeePercent; - remainder -= payment; - - // Handle Ether - if (tokenAddress == address(0)) { - SafeTransferLib.safeTransferETH(pmtData.payee, payment); - - // Handle ERC20 - } else { - SafeTransferLib.safeTransfer(ERC20(tokenAddress), pmtData.payee, payment); - } - - unchecked { - ++k; - } - } - - unchecked { - ++i; - } - } - } -} diff --git a/src/contracts/protocol/ProtocolControl.sol b/src/contracts/protocol/ProtocolControl.sol index c304159e5..6424bf445 100644 --- a/src/contracts/protocol/ProtocolControl.sol +++ b/src/contracts/protocol/ProtocolControl.sol @@ -7,13 +7,13 @@ import {IExecutionEnvironment} from "../interfaces/IExecutionEnvironment.sol"; import {CallBits} from "../libraries/CallBits.sol"; import {GovernanceControl} from "./GovernanceControl.sol"; -import {MEVAllocator} from "./MEVAllocator.sol"; +import {ExecutionBase} from "./ExecutionBase.sol"; import "../types/CallTypes.sol"; import "forge-std/Test.sol"; -abstract contract ProtocolControl is Test, MEVAllocator, GovernanceControl { +abstract contract ProtocolControl is Test, GovernanceControl, ExecutionBase { address public immutable escrow; address public immutable governance; address public immutable control; @@ -45,19 +45,6 @@ abstract contract ProtocolControl is Test, MEVAllocator, GovernanceControl { sequenced = shouldRequireSequencedNonces; - /* - // Disallow delegatecall when recycled storage is used - if(allowRecycledStorage) { - require( - ( - (!shouldDelegateStaging) && - (!shouldDelegateUser) && - (!shouldDelegateVerification) - ), - "ERR-GC01 DelegatingWithRecyled" - ); - } - */ if (shouldDelegateStaging) { require(shouldRequireStaging, "ERR-GC04 InvalidStaging"); @@ -67,10 +54,6 @@ abstract contract ProtocolControl is Test, MEVAllocator, GovernanceControl { require(shouldRequireVerification, "ERR-GC05 InvalidVerification"); } - // NOTE: At this time, MEV Allocation payments are required to be delegatecalled. - // By the time the MEV Payments are paid, both the user and the searchers will - // no longer be executing any transactions, and all MEV rewards will be - // held in the ExecutionEnvironment require(shouldDelegateAllocating, "ERR-GC02 NotDelegateAllocating"); if (shouldDelegateUser) { @@ -107,11 +90,7 @@ abstract contract ProtocolControl is Test, MEVAllocator, GovernanceControl { onlyApprovedCaller(delegateStaging) returns (bytes memory) { - return ( - delegateStaging - ? _stageDelegateCall(to, from, userSelector, userData) - : _stageStaticCall(to, from, userSelector, userData) - ); + return _stageCall(to, from, userSelector, userData); } function userLocalCall(bytes calldata data) external onlyApprovedCaller(delegateUser) returns (bytes memory) { @@ -119,11 +98,11 @@ abstract contract ProtocolControl is Test, MEVAllocator, GovernanceControl { } function allocatingCall(bytes calldata data) external onlyApprovedCaller(true) { - return _allocatingDelegateCall(data); + return _allocatingCall(data); } function verificationCall(bytes calldata data) external onlyApprovedCaller(delegateVerification) returns (bool) { - return delegateVerification ? _verificationDelegateCall(data) : _verificationStaticCall(data); + return _verificationCall(data); } // View functions diff --git a/src/contracts/types/CallTypes.sol b/src/contracts/types/CallTypes.sol index 5880014cb..abcd7c753 100644 --- a/src/contracts/types/CallTypes.sol +++ b/src/contracts/types/CallTypes.sol @@ -22,8 +22,9 @@ struct SearcherMetaTx { uint256 value; uint256 gas; uint256 nonce; - bytes32 userCallHash; // hash of user EOA and calldata, for verification of user's tx (if not matched, searcher wont be charged for gas) uint256 maxFeePerGas; // maxFeePerGas searcher is willing to pay. This goes to validator, not protocol or user + bytes32 userCallHash; // hash of user EOA and calldata, for verification of user's tx (if not matched, searcher wont be charged for gas) + bytes32 controlCodeHash; // ProtocolControl.codehash bytes32 bidsHash; // searcher's backend must keccak256() their BidData array and include that in the signed meta tx, which we then verify on chain. bytes data; } diff --git a/src/contracts/types/EscrowTypes.sol b/src/contracts/types/EscrowTypes.sol index d2ccf9cfa..9eeb85661 100644 --- a/src/contracts/types/EscrowTypes.sol +++ b/src/contracts/types/EscrowTypes.sol @@ -57,5 +57,5 @@ enum SearcherOutcome } bytes32 constant SEARCHER_TYPE_HASH = keccak256( - "SearcherMetaTx(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes32 userCallHash,uint256 maxFeePerGas,bytes32 bidsHash,bytes data)" + "SearcherMetaTx(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes32 userCallHash,bytes32 controlCodeHash,uint256 maxFeePerGas,bytes32 bidsHash,bytes data)" ); diff --git a/src/contracts/types/VerificationTypes.sol b/src/contracts/types/VerificationTypes.sol index 718d75511..fbc89a23c 100644 --- a/src/contracts/types/VerificationTypes.sol +++ b/src/contracts/types/VerificationTypes.sol @@ -19,4 +19,5 @@ struct ProtocolProof { uint256 deadline; bytes32 userCallHash; // keccak256 of userCall.to, userCall.data bytes32 callChainHash; // keccak256 of the searchers' txs + bytes32 controlCodeHash; // ProtocolControl.codehash } diff --git a/src/contracts/v2-example/V2ProtocolControl.sol b/src/contracts/v2-example/V2ProtocolControl.sol index 517f6ee4b..2f6983887 100644 --- a/src/contracts/v2-example/V2ProtocolControl.sol +++ b/src/contracts/v2-example/V2ProtocolControl.sol @@ -15,7 +15,6 @@ import "../types/LockTypes.sol"; // Atlas Protocol-Control Imports import {ProtocolControl} from "../protocol/ProtocolControl.sol"; -import {MEVAllocator} from "../protocol/MEVAllocator.sol"; // Uni V2 Imports import {IUniswapV2Pair} from "./interfaces/IUniswapV2Pair.sol"; @@ -30,7 +29,7 @@ interface IWETH { function withdraw(uint256 wad) external; } -contract V2ProtocolControl is MEVAllocator, ProtocolControl { +contract V2ProtocolControl is ProtocolControl { uint256 public constant CONTROL_GAS_USAGE = 250_000; @@ -48,7 +47,6 @@ contract V2ProtocolControl is MEVAllocator, ProtocolControl { event GiftedGovernanceToken(address indexed user, address indexed token, uint256 amount); constructor(address _escrow) - MEVAllocator() ProtocolControl(_escrow, msg.sender, false, true, true, false, false, true, false, false, true) { govIsTok0 = (IUniswapV2Pair(WETH_X_GOVERNANCE_POOL).token0() == GOVERNANCE_TOKEN); @@ -75,16 +73,13 @@ contract V2ProtocolControl is MEVAllocator, ProtocolControl { ) */ - function _stageDelegateCall(address to, address from, bytes4 userSelector, bytes calldata userData) + function _stageCall(address to, address, bytes4 userSelector, bytes calldata userData) internal override returns (bytes memory) { require(userSelector == SWAP, "ERR-H10 InvalidFunction"); - // NOTE: This is a very direct example to facilitate the creation of a testing environment. - // Using this example in production is ill-advised. - ( uint256 amount0Out, uint256 amount1Out, @@ -92,53 +87,20 @@ contract V2ProtocolControl is MEVAllocator, ProtocolControl { // bytes memory swapData // Unused ) = abi.decode(userData, (uint256, uint256, address, bytes)); - /* - address tokenUserIsSelling = amount0Out > amount1Out ? - IUniswapV2Pair(to).token1() : - IUniswapV2Pair(to).token0() ; - */ - (uint112 token0Balance, uint112 token1Balance,) = IUniswapV2Pair(to).getReserves(); - /* - // ENABLE FOR FOUNDRY TESTING - console.log("---"); - console.log("protocolControlExecutingAs",address(this)); - console.log("---"); - console.log("amount0Out ", amount0Out); - console.log("token0Balance", token0Balance); - console.log("---"); - console.log("amount1Out ", amount1Out); - console.log("token1Balance", token1Balance); - console.log("---"); - */ - uint256 amount0In = amount1Out == 0 ? 0 : SwapMath.getAmountIn(amount1Out, uint256(token0Balance), uint256(token1Balance)); uint256 amount1In = amount0Out == 0 ? 0 : SwapMath.getAmountIn(amount0Out, uint256(token1Balance), uint256(token0Balance)); - // uint256 amountUserIsSelling = amount0Out > amount1Out ? amount1In : amount0In; - - /* - // ENABLE FOR FOUNDRY TESTING - if (amount0Out > amount1Out) { - console.log("amount1In ", amount1In); - console.log("userBalance", ERC20(tokenUserIsSelling).balanceOf(userCallFrom)); - console.log("EEAllowance", ERC20(tokenUserIsSelling).allowance(userCallFrom, address(this))); - console.log("---"); - } else { - console.log("amount0In ", amount0In); - console.log("userBalance", ERC20(tokenUserIsSelling).balanceOf(userCallFrom)); - console.log("EEAllowance", ERC20(tokenUserIsSelling).allowance(userCallFrom, address(this))); - console.log("---"); - } - */ // This is a V2 swap, so optimistically transfer the tokens // NOTE: The user should have approved the ExecutionEnvironment for token transfers - ERC20(amount0Out > amount1Out ? IUniswapV2Pair(to).token1() : IUniswapV2Pair(to).token0()).transferFrom( - from, to, amount0In > amount1In ? amount0In : amount1In + _transferUserERC20( + amount0Out > amount1Out ? IUniswapV2Pair(to).token1() : IUniswapV2Pair(to).token0(), + to, + amount0In > amount1In ? amount0In : amount1In ); bytes memory emptyData; @@ -147,7 +109,7 @@ contract V2ProtocolControl is MEVAllocator, ProtocolControl { // This occurs after a Searcher has successfully paid their bid, which is // held in ExecutionEnvironment. - function _allocatingDelegateCall(bytes calldata) internal override { + function _allocatingCall(bytes calldata) internal override { // This function is delegatecalled // address(this) = ExecutionEnvironment // msg.sender = Escrow @@ -155,7 +117,7 @@ contract V2ProtocolControl is MEVAllocator, ProtocolControl { // NOTE: ProtocolVerifier has verified the BidData[] format // BidData[0] = address(WETH) <== WETH - address user = IExecutionEnvironment(address(this)).getUser(); + address user = _user(); // MEV Rewards were collected in WETH uint256 balance = ERC20(WETH).balanceOf(address(this)); diff --git a/src/contracts/v4-example/IHooks.sol b/src/contracts/v4-example/IHooks.sol new file mode 100644 index 000000000..b9425c162 --- /dev/null +++ b/src/contracts/v4-example/IHooks.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.18; + +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; + } +} \ No newline at end of file diff --git a/src/contracts/v4-example/IPoolManager.sol b/src/contracts/v4-example/IPoolManager.sol new file mode 100644 index 000000000..0469f18d9 --- /dev/null +++ b/src/contracts/v4-example/IPoolManager.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.18; + +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); +} \ No newline at end of file diff --git a/src/contracts/v4-example/UniV4Hook.sol b/src/contracts/v4-example/UniV4Hook.sol index 676ee893b..3c448c446 100644 --- a/src/contracts/v4-example/UniV4Hook.sol +++ b/src/contracts/v4-example/UniV4Hook.sol @@ -5,334 +5,38 @@ pragma solidity ^0.8.16; import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; // V4 Imports -// import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; -// import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -// import {BaseHook} from "@uniswap/periphery-next/contracts/BaseHook.sol"; +import {IPoolManager} from "./IPoolManager.sol"; +import {IHooks} from "./IHooks.sol"; + +// Atlas Imports +import {V4ProtocolControl} from "./V4ProtocolControl.sol"; -// Atlas Base Imports import {ISafetyLocks} from "../interfaces/ISafetyLocks.sol"; import {SafetyBits} from "../libraries/SafetyBits.sol"; import "../types/CallTypes.sol"; import "../types/LockTypes.sol"; -// Atlas Protocol-Control Imports -import {ProtocolControl} from "../protocol/ProtocolControl.sol"; -import {MEVAllocator} from "../protocol/MEVAllocator.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; - } - - function swap(PoolKey memory key, SwapParams memory params) external returns (BalanceDelta); - function donate(PoolKey memory key, uint256 amount0, uint256 amount1) external returns (BalanceDelta); -} - -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; - } -} - // 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. -contract AtlasV4Hook is MEVAllocator, ProtocolControl { - struct StagingReturn { - 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; - - // Storage lock - // keccak256(poolKey, executionEnvironment) - bytes32 public hashLock; // TODO: Transient storage <- - - // 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; - - PoolKey internal _currentKey; // TODO: Transient storage <- - - constructor(address _escrow, address _v4Singleton) - MEVAllocator() - ProtocolControl(_escrow, msg.sender, true, true, true, false, false, true, true, true, false) - { - /* - ProtocolControl( - _escrow, // escrowAddress - true, // shouldRequireSequencedNonces - true, // shouldRequireStaging - true, // shouldDelegateStaging - false, // shouldExecuteUserLocally - false, // shouldDelegateUser - true, // shouldDelegateAllocating - true, // shouldRequireVerification - true, // shouldDelegateVerification - false // allowRecycledStorage - ) - */ - - hook = address(this); - v4Singleton = _v4Singleton; - } ///////////////////////////////////////////////////////// - // ATLAS CALLS // + // V4 HOOK // ///////////////////////////////////////////////////////// - /////////////// DELEGATED CALLS ////////////////// - function _stageDelegateCall(address to, address from, bytes4 userSelector, bytes calldata userData) - internal - override - returns (bytes memory stagingData) - { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Atlas Escrow - - - require(!_currentKey.initialized, "ERR-H09 AlreadyInitialized"); - - require(userSelector == SWAP, "ERR-H10 InvalidFunction"); - - UserCall memory userCall = abi.decode(userData, (UserCall)); - - require(to == v4Singleton, "ERR-H11 InvalidTo"); - - // Verify that the swapper went through the FastLane Atlas MEV Auction - // and that ProtocolControl supplied a valid signature - require(msg.sender == escrow, "ERR-H00 InvalidCaller"); - - - (IPoolManager.PoolKey memory key, IPoolManager.SwapParams memory params) = - abi.decode(userCall.data, (IPoolManager.PoolKey, IPoolManager.SwapParams)); - - // Perform more checks and activate the lock - AtlasV4Hook(hook).setLock(key); - - // Store the key so that we can access it at verification - _currentKey = PoolKey({ - initialized: true, // TODO: consider using a lock array like v4 so we can handle multiple? - key: key - }); - - // Handle forwarding of token approvals, or token transfers. - // NOTE: The user will have approved the ExecutionEnvironment in a prior call - StagingReturn memory stagingReturn = StagingReturn({ - 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( - ERC20(IPoolManager.Currency.unwrap(key.currency0)), - 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 - stagingData = abi.encode(stagingReturn); - } - - // This occurs after a Searcher has successfully paid their bid, which is - // held in ExecutionEnvironment. - function _allocatingDelegateCall(bytes calldata data) internal override { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Escrow - - require(!_currentKey.initialized, "ERR-H09 AlreadyInitialized"); - - // Pull the calldata into memory - (, BidData[] memory bids) = abi.decode(data, (uint256, BidData[])); - - // NOTE: ProtocolVerifier has verified the BidData[] format - // BidData[0] = token0 - // BidData[1] = token1 - - uint256 token0DonateAmount = bids[0].bidAmount; - uint256 token1DonateAmount = bids[1].bidAmount; - - IPoolManager.PoolKey memory key = _currentKey.key; - - IPoolManager(v4Singleton).donate(key, token0DonateAmount, token1DonateAmount); - - // 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 _verificationDelegateCall(bytes calldata data) internal override returns (bool) { - // This function is delegatecalled - // address(this) = ExecutionEnvironment - // msg.sender = Escrow - - ( - bytes memory stagingReturnData, - //bytes memory userReturnData - ) = abi.decode(data, (bytes, bytes)); - - StagingReturn memory stagingReturn = abi.decode(stagingReturnData, (StagingReturn)); - - AtlasV4Hook(hook).releaseLock(stagingReturn.poolKey); - - delete _currentKey; - - return true; - } - - /////////////// EXTERNAL CALLS ////////////////// - function setLock(IPoolManager.PoolKey memory key) external { - // This function is a standard call - // address(this) = hook - // msg.sender = ExecutionEnvironment - - EscrowKey memory escrowKey = ISafetyLocks(escrow).getLockState(); - - // Verify that the swapper went through the FastLane Atlas MEV Auction - // and that ProtocolControl supplied a valid signature - require(address(this) == hook, "ERR-H00 InvalidCallee"); - require(hook == escrowKey.approvedCaller, "ERR-H01 InvalidCaller"); - require(escrowKey.lockState == SafetyBits._LOCKED_X_STAGING_X_UNSET, "ERR-H02 InvalidLockStage"); - require(hashLock == bytes32(0), "ERR-H03 AlreadyActive"); - - // Set the storage lock to block reentry / concurrent trading - hashLock = keccak256(abi.encode(key, msg.sender)); - } - - function releaseLock(IPoolManager.PoolKey memory key) external { - // This function is a standard call - // address(this) = hook - // msg.sender = ExecutionEnvironment - - EscrowKey memory escrowKey = ISafetyLocks(escrow).getLockState(); - - // Verify that the swapper went through the FastLane Atlas MEV Auction - // and that ProtocolControl supplied a valid signature - require(address(this) == hook, "ERR-H20 InvalidCallee"); - require(hook == escrowKey.approvedCaller, "ERR-H21 InvalidCaller"); - require(escrowKey.lockState == SafetyBits._LOCKED_X_VERIFICATION_X_UNSET, "ERR-H22 InvalidLockStage"); - require(hashLock == keccak256(abi.encode(key, msg.sender)), "ERR-H23 InvalidKey"); - - // Release the storage lock - delete hashLock; - } - - ///////////////// GETTERS & HELPERS // ////////////////// - function getPayeeData(bytes calldata data) external pure override returns (PayeeData[] memory) { - // This function is called by the backend to get the - // payee data, and by the Atlas Factory to generate a - // hash to verify the backend. - - IPoolManager.PoolKey memory key = abi.decode(data, (IPoolManager.PoolKey)); - - PaymentData[] memory payments = new PaymentData[](1); - - payments[0] = PaymentData({payee: address(key.hooks), payeePercent: 100}); - - PayeeData[] memory payeeData = new PayeeData[](2); - - payeeData[0] = PayeeData({token: IPoolManager.Currency.unwrap(key.currency0), payments: payments, data: data}); - - payeeData[1] = PayeeData({token: IPoolManager.Currency.unwrap(key.currency1), payments: payments, data: data}); - - return payeeData; - } - - function getBidFormat(bytes calldata data) external pure override returns (BidData[] memory) { - // This is a helper function called by searchers - // so that they can get the proper format for - // submitting their bids to the hook. - - IPoolManager.PoolKey memory key = abi.decode(data, (IPoolManager.PoolKey)); - - BidData[] memory bidData = new BidData[](2); - - bidData[0] = BidData({ - token: IPoolManager.Currency.unwrap(key.currency0), - bidAmount: 0 // <- searcher must update - }); - - bidData[1] = BidData({ - token: IPoolManager.Currency.unwrap(key.currency1), - bidAmount: 0 // <- searcher must update - }); - - return bidData; - } - - ///////////////////////////////////////////////////////// - // V4 HOOKS // - ///////////////////////////////////////////////////////// +contract UniV4Hook is V4ProtocolControl { + constructor(address _escrow, address _v4Singleton) V4ProtocolControl(_escrow, _v4Singleton) {} + function getHooksCalls() public pure returns (IHooks.Calls memory) { // override return IHooks.Calls({ beforeInitialize: false, afterInitialize: false, - beforeModifyPosition: false, + beforeModifyPosition: true, // <-- afterModifyPosition: false, beforeSwap: true, // <-- afterSwap: false, @@ -341,6 +45,15 @@ contract AtlasV4Hook is MEVAllocator, ProtocolControl { }); } + 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 @@ -402,6 +115,6 @@ contract AtlasV4Hook is MEVAllocator, ProtocolControl { // the hashLock's value. It should not be used as a lock to keep them out - it is only // meant to prevent searchers from winning an auction for Pool X but trading in Pool Y. - return AtlasV4Hook.beforeSwap.selector; + return UniV4Hook.beforeSwap.selector; } } diff --git a/src/contracts/v4-example/V4ProtocolControl.sol b/src/contracts/v4-example/V4ProtocolControl.sol new file mode 100644 index 000000000..69cb43115 --- /dev/null +++ b/src/contracts/v4-example/V4ProtocolControl.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.18; + +// Base Imports +import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; + +// Atlas Base Imports +import {ISafetyLocks} from "../interfaces/ISafetyLocks.sol"; +import {SafetyBits} from "../libraries/SafetyBits.sol"; + +import "../types/CallTypes.sol"; +import "../types/LockTypes.sol"; + +// Atlas Protocol-Control Imports +import {ProtocolControl} from "../protocol/ProtocolControl.sol"; + +// V4 Imports +import {IPoolManager} from "./IPoolManager.sol"; +import {IHooks} from "./IHooks.sol"; + +contract V4ProtocolControl is ProtocolControl { + struct StagingReturn { + 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; + + // Storage lock + // keccak256(poolKey, executionEnvironment) + bytes32 public hashLock; // TODO: Transient storage <- + + // 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; + + PoolKey internal _currentKey; // TODO: Transient storage <- + + constructor(address _escrow, address _v4Singleton) + ProtocolControl(_escrow, msg.sender, true, true, true, false, false, true, true, true, false) + { + /* + ProtocolControl( + _escrow, // escrowAddress + true, // shouldRequireSequencedNonces + true, // shouldRequireStaging + true, // shouldDelegateStaging + false, // shouldExecuteUserLocally + false, // shouldDelegateUser + true, // shouldDelegateAllocating + true, // shouldRequireVerification + true, // shouldDelegateVerification + false // allowRecycledStorage + ) + */ + + hook = address(this); + v4Singleton = _v4Singleton; + } + + ///////////////////////////////////////////////////////// + // ATLAS CALLS // + ///////////////////////////////////////////////////////// + + /////////////// DELEGATED CALLS ////////////////// + function _stageCall(address to, address from, bytes4 userSelector, bytes calldata userData) + internal + override + returns (bytes memory stagingData) + { + // This function is delegatecalled + // address(this) = ExecutionEnvironment + // msg.sender = Atlas Escrow + + + require(!_currentKey.initialized, "ERR-H09 AlreadyInitialized"); + + require(userSelector == SWAP, "ERR-H10 InvalidFunction"); + + UserCall memory userCall = abi.decode(userData, (UserCall)); + + require(to == v4Singleton, "ERR-H11 InvalidTo"); + + // Verify that the swapper went through the FastLane Atlas MEV Auction + // and that ProtocolControl supplied a valid signature + require(msg.sender == escrow, "ERR-H00 InvalidCaller"); + + + (IPoolManager.PoolKey memory key, IPoolManager.SwapParams memory params) = + abi.decode(userCall.data, (IPoolManager.PoolKey, IPoolManager.SwapParams)); + + // Perform more checks and activate the lock + V4ProtocolControl(hook).setLock(key); + + // Store the key so that we can access it at verification + _currentKey = PoolKey({ + initialized: true, // TODO: consider using a lock array like v4 so we can handle multiple? + key: key + }); + + // Handle forwarding of token approvals, or token transfers. + // NOTE: The user will have approved the ExecutionEnvironment in a prior call + StagingReturn memory stagingReturn = StagingReturn({ + 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( + ERC20(IPoolManager.Currency.unwrap(key.currency0)), + 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 + stagingData = abi.encode(stagingReturn); + } + + // This occurs after a Searcher has successfully paid their bid, which is + // held in ExecutionEnvironment. + function _allocatingCall(bytes calldata data) internal override { + // This function is delegatecalled + // address(this) = ExecutionEnvironment + // msg.sender = Escrow + + require(!_currentKey.initialized, "ERR-H09 AlreadyInitialized"); + + // Pull the calldata into memory + (, BidData[] memory bids) = abi.decode(data, (uint256, BidData[])); + + // NOTE: ProtocolVerifier has verified the BidData[] format + // BidData[0] = token0 + // BidData[1] = token1 + + uint256 token0DonateAmount = bids[0].bidAmount; + uint256 token1DonateAmount = bids[1].bidAmount; + + IPoolManager.PoolKey memory key = _currentKey.key; + + IPoolManager(v4Singleton).donate(key, token0DonateAmount, token1DonateAmount); + + // 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 _verificationCall(bytes calldata data) internal override returns (bool) { + // This function is delegatecalled + // address(this) = ExecutionEnvironment + // msg.sender = Escrow + + ( + bytes memory stagingReturnData, + //bytes memory userReturnData + ) = abi.decode(data, (bytes, bytes)); + + StagingReturn memory stagingReturn = abi.decode(stagingReturnData, (StagingReturn)); + + V4ProtocolControl(hook).releaseLock(stagingReturn.poolKey); + + delete _currentKey; + + return true; + } + + /////////////// EXTERNAL CALLS ////////////////// + function setLock(IPoolManager.PoolKey memory key) external { + // This function is a standard call + // address(this) = hook + // msg.sender = ExecutionEnvironment + + EscrowKey memory escrowKey = ISafetyLocks(escrow).getLockState(); + + // Verify that the swapper went through the FastLane Atlas MEV Auction + // and that ProtocolControl supplied a valid signature + require(address(this) == hook, "ERR-H00 InvalidCallee"); + require(hook == escrowKey.approvedCaller, "ERR-H01 InvalidCaller"); + require(escrowKey.lockState == SafetyBits._LOCKED_X_STAGING_X_UNSET, "ERR-H02 InvalidLockStage"); + require(hashLock == bytes32(0), "ERR-H03 AlreadyActive"); + + // Set the storage lock to block reentry / concurrent trading + hashLock = keccak256(abi.encode(key, msg.sender)); + } + + function releaseLock(IPoolManager.PoolKey memory key) external { + // This function is a standard call + // address(this) = hook + // msg.sender = ExecutionEnvironment + + EscrowKey memory escrowKey = ISafetyLocks(escrow).getLockState(); + + // Verify that the swapper went through the FastLane Atlas MEV Auction + // and that ProtocolControl supplied a valid signature + require(address(this) == hook, "ERR-H20 InvalidCallee"); + require(hook == escrowKey.approvedCaller, "ERR-H21 InvalidCaller"); + require(escrowKey.lockState == SafetyBits._LOCKED_X_VERIFICATION_X_UNSET, "ERR-H22 InvalidLockStage"); + require(hashLock == keccak256(abi.encode(key, msg.sender)), "ERR-H23 InvalidKey"); + + // Release the storage lock + delete hashLock; + } + + ///////////////// GETTERS & HELPERS // ////////////////// + function getPayeeData(bytes calldata data) external pure override returns (PayeeData[] memory) { + // This function is called by the backend to get the + // payee data, and by the Atlas Factory to generate a + // hash to verify the backend. + + IPoolManager.PoolKey memory key = abi.decode(data, (IPoolManager.PoolKey)); + + PaymentData[] memory payments = new PaymentData[](1); + + payments[0] = PaymentData({payee: address(key.hooks), payeePercent: 100}); + + PayeeData[] memory payeeData = new PayeeData[](2); + + payeeData[0] = PayeeData({token: IPoolManager.Currency.unwrap(key.currency0), payments: payments, data: data}); + + payeeData[1] = PayeeData({token: IPoolManager.Currency.unwrap(key.currency1), payments: payments, data: data}); + + return payeeData; + } + + function getBidFormat(bytes calldata data) external pure override returns (BidData[] memory) { + // This is a helper function called by searchers + // so that they can get the proper format for + // submitting their bids to the hook. + + IPoolManager.PoolKey memory key = abi.decode(data, (IPoolManager.PoolKey)); + + BidData[] memory bidData = new BidData[](2); + + bidData[0] = BidData({ + token: IPoolManager.Currency.unwrap(key.currency0), + bidAmount: 0 // <- searcher must update + }); + + bidData[1] = BidData({ + token: IPoolManager.Currency.unwrap(key.currency1), + bidAmount: 0 // <- searcher must update + }); + + return bidData; + } +} \ No newline at end of file diff --git a/test/Helpers.sol b/test/Helpers.sol index d138b8053..2b1dd09b9 100644 --- a/test/Helpers.sol +++ b/test/Helpers.sol @@ -86,6 +86,7 @@ contract Helper is Test, TestConstants { function buildSearcherCall( UserCall memory userCall, + ProtocolCall memory protocolCall, address searcherEOA, address searcherContract, address poolOne, @@ -99,8 +100,9 @@ contract Helper is Test, TestConstants { gas: gas, value: 0, nonce: searcherNextNonce(searcherEOA), - userCallHash: keccak256(abi.encodePacked(userCall.to, userCall.data)), maxFeePerGas: maxFeePerGas, + userCallHash: keccak256(abi.encodePacked(userCall.to, userCall.data)), + controlCodeHash: protocolCall.to.codehash, bidsHash: keccak256(abi.encode(searcherCall.bids)), data: abi.encodeWithSelector(BlindBackrun.executeArbitrage.selector, poolOne, poolTwo) }); @@ -121,7 +123,8 @@ contract Helper is Test, TestConstants { nonce: governanceNextNonce(governanceEOA), deadline: deadline, userCallHash: keccak256(abi.encodePacked(userCall.to, userCall.data)), - callChainHash: executionHashChain[executionHashChain.length - 1] + callChainHash: executionHashChain[executionHashChain.length - 1], + controlCodeHash: protocolCall.to.codehash }); } } diff --git a/test/MainTest.t.sol b/test/MainTest.t.sol index 7f5a8399d..c6a00c531 100644 --- a/test/MainTest.t.sol +++ b/test/MainTest.t.sol @@ -49,14 +49,14 @@ contract MainTest is BaseTest { // First SearcherCall searcherCalls[0] = - helper.buildSearcherCall(userCall, searcherOneEOA, address(searcherOne), POOL_ONE, POOL_TWO, 2e17); + helper.buildSearcherCall(userCall, protocolCall, searcherOneEOA, address(searcherOne), POOL_ONE, POOL_TWO, 2e17); (v, r, s) = vm.sign(searcherOnePK, IAtlas(address(atlas)).getSearcherPayload(searcherCalls[0].metaTx)); searcherCalls[0].signature = abi.encodePacked(r, s, v); // Second SearcherCall searcherCalls[1] = - helper.buildSearcherCall(userCall, searcherTwoEOA, address(searcherTwo), POOL_TWO, POOL_ONE, 1e17); + helper.buildSearcherCall(userCall, protocolCall, searcherTwoEOA, address(searcherTwo), POOL_TWO, POOL_ONE, 1e17); (v, r, s) = vm.sign(searcherTwoPK, IAtlas(address(atlas)).getSearcherPayload(searcherCalls[1].metaTx)); searcherCalls[1].signature = abi.encodePacked(r, s, v); @@ -80,8 +80,8 @@ contract MainTest is BaseTest { console.log("executionEnvironment", executionEnvironment); // User must approve the execution environment - ERC20(TOKEN_ZERO).approve(executionEnvironment, type(uint256).max); - ERC20(TOKEN_ONE).approve(executionEnvironment, type(uint256).max); + ERC20(TOKEN_ZERO).approve(address(atlas), type(uint256).max); + ERC20(TOKEN_ONE).approve(address(atlas), type(uint256).max); uint256 userBalance = userEOA.balance; @@ -95,6 +95,56 @@ contract MainTest is BaseTest { console.log("user gas refund received",userEOA.balance - userBalance); console.log("user refund equivalent gas usage", (userEOA.balance - userBalance)/tx.gasprice); + vm.stopPrank(); + + console.log(""); + console.log("-"); + console.log("-"); + + // Second attempt + protocolCall = helper.getProtocolCall(); + + userCall = helper.buildUserCall(POOL_ONE, userEOA, TOKEN_ONE); + + // First SearcherCall + searcherCalls[0] = + helper.buildSearcherCall(userCall, protocolCall, searcherOneEOA, address(searcherOne), POOL_ONE, POOL_TWO, 2e17); + + (v, r, s) = vm.sign(searcherOnePK, IAtlas(address(atlas)).getSearcherPayload(searcherCalls[0].metaTx)); + searcherCalls[0].signature = abi.encodePacked(r, s, v); + + // Second SearcherCall + searcherCalls[1] = + helper.buildSearcherCall(userCall, protocolCall, searcherTwoEOA, address(searcherTwo), POOL_TWO, POOL_ONE, 1e17); + + (v, r, s) = vm.sign(searcherTwoPK, IAtlas(address(atlas)).getSearcherPayload(searcherCalls[1].metaTx)); + searcherCalls[1].signature = abi.encodePacked(r, s, v); + + // Verification call + verification = + helper.buildVerification(governanceEOA, protocolCall, userCall, searcherCalls); + + (v, r, s) = vm.sign(governancePK, IAtlas(address(atlas)).getVerificationPayload(verification)); + + verification.signature = abi.encodePacked(r, s, v); + + vm.startPrank(userEOA); + + executionEnvironment = IAtlas(address(atlas)).getExecutionEnvironment(userCall, address(control)); + + userBalance = userEOA.balance; + + (success,) = address(atlas).call( + abi.encodeWithSelector( + atlas.metacall.selector, protocolCall, userCall, searcherCalls, verification + ) + ); + + assertTrue(success); + console.log("user gas refund received",userEOA.balance - userBalance); + console.log("user refund equivalent gas usage", (userEOA.balance - userBalance)/tx.gasprice); + + vm.stopPrank(); } /*