diff --git a/src/contracts/atlas/Atlas.sol b/src/contracts/atlas/Atlas.sol index 4d6e3be33..2c78a0c42 100644 --- a/src/contracts/atlas/Atlas.sol +++ b/src/contracts/atlas/Atlas.sol @@ -105,7 +105,15 @@ contract Atlas is Escrow, Factory { ); } catch (bytes memory revertData) { // Bubble up some specific errors - _handleErrors(revertData, _dConfig.callConfig); + bool _doUserOpFailedHook = _handleErrors(revertData, _dConfig.callConfig); + + // If appropriate, execute the userOpFailed hook. If it reverts, let the whole metacall revert. + if (_doUserOpFailedHook) { + _executeUserOpFailedHook( + _buildUserOpFailedContext(_executionEnvironment, _bundler, _isSimulation), userOp + ); + } + // Set lock to FullyLocked to prevent any reentrancy possibility _setLockPhase(uint8(ExecutionPhase.FullyLocked)); @@ -320,7 +328,8 @@ contract Atlas is Escrow, Factory { /// @notice Called at the end of `metacall` to bubble up specific error info in a revert. /// @param revertData Revert data from a failure during the execution of the metacall. /// @param callConfig The CallConfig of the current metacall tx. - function _handleErrors(bytes memory revertData, uint32 callConfig) internal view { + /// @return A boolean indicating whether the error was a UserOpFail, which should trigger the userOpFailed hook. + function _handleErrors(bytes memory revertData, uint32 callConfig) internal view returns (bool) { bytes4 _errorSwitch = bytes4(revertData); if (msg.sender == SIMULATOR) { // Simulation @@ -342,6 +351,11 @@ contract Atlas is Escrow, Factory { revert PostOpsSimFail(); } } + if (_errorSwitch == UserOpFail.selector) { + // TODO check callConfig.needsUserOpFailedHook() if we include that setting + // TODO ensure needsUserOpFailedHook and allowsReuseUserOps are mutually exclusive + return true; + } if (_errorSwitch == UserNotFulfilled.selector) { revert UserNotFulfilled(); } @@ -355,6 +369,9 @@ contract Atlas is Escrow, Factory { revert(0, 4) } } + + // If we reach this, do not trigger the userOpFailed hook. + return false; } /// @notice Returns whether or not the execution environment address matches what's expected from the set of inputs. diff --git a/src/contracts/atlas/Escrow.sol b/src/contracts/atlas/Escrow.sol index 0f04581d3..ee0fc311e 100644 --- a/src/contracts/atlas/Escrow.sol +++ b/src/contracts/atlas/Escrow.sol @@ -131,6 +131,26 @@ abstract contract Escrow is AtlETH { revert UserOpFail(); } + function _executeUserOpFailedHook( + Context memory ctx, + UserOperation calldata userOp + ) + internal + withLockPhase(ExecutionPhase.UserOpFailed) + { + (bool _success,) = ctx.executionEnvironment.call( + abi.encodePacked( + abi.encodeCall(IExecutionEnvironment.userOpFailedWrapper, userOp), + ctx.setAndPack(ExecutionPhase.UserOpFailed) + ) + ); + + if (!_success) { + if (ctx.isSimulation) revert UserOpFailedHookSimFail(); + revert UserOpFailedHookFail(); + } + } + /// @notice Checks if the trusted operation hash matches and sets the appropriate error bit if it doesn't. /// @param dConfig Configuration data for the DApp involved, containing execution parameters and settings. /// @param prevalidated Boolean flag indicating whether the SolverOperation has been prevalidated to skip certain diff --git a/src/contracts/atlas/SafetyLocks.sol b/src/contracts/atlas/SafetyLocks.sol index 87f274eea..bd7ab46e4 100644 --- a/src/contracts/atlas/SafetyLocks.sol +++ b/src/contracts/atlas/SafetyLocks.sol @@ -89,4 +89,29 @@ abstract contract SafetyLocks is Storage { callDepth: 0 }); } + + function _buildUserOpFailedContext( + address executionEnvironment, + address bundler, + bool isSimulation + ) + internal + pure + returns (Context memory) + { + return Context({ + executionEnvironment: executionEnvironment, + userOpHash: bytes32(0), + bundler: bundler, + solverSuccessful: false, + paymentsSuccessful: false, + solverIndex: 0, + solverCount: 0, + phase: uint8(ExecutionPhase.UserOpFailed), + solverOutcome: 0, + bidFind: false, + isSimulation: isSimulation, + callDepth: 0 + }); + } } diff --git a/src/contracts/common/ExecutionEnvironment.sol b/src/contracts/common/ExecutionEnvironment.sol index 84fb2fc03..36da520d6 100644 --- a/src/contracts/common/ExecutionEnvironment.sol +++ b/src/contracts/common/ExecutionEnvironment.sol @@ -86,6 +86,15 @@ contract ExecutionEnvironment is Base { } } + function userOpFailedWrapper(UserOperation calldata userOp) external onlyAtlasEnvironment { + bytes memory _data = _forward(abi.encodeCall(IDAppControl.userOpFailedCall, userOp)); + bool _success; + + (_success,) = _control().delegatecall(_data); + + if (!_success) revert AtlasErrors.UserOpFailedWrapperDelegatecallFail(); + } + /// @notice The postOpsWrapper function may be called by Atlas as the last phase of a `metacall` transaction. /// @dev This contract is called by the Atlas contract, and delegatecalls the DAppControl contract via the /// corresponding `postOpsCall` function. diff --git a/src/contracts/dapp/ControlTemplate.sol b/src/contracts/dapp/ControlTemplate.sol index 3663f915e..7501bf80a 100644 --- a/src/contracts/dapp/ControlTemplate.sol +++ b/src/contracts/dapp/ControlTemplate.sol @@ -61,6 +61,28 @@ abstract contract DAppControlTemplate { // User exposure: Trustless function _checkUserOperation(UserOperation memory) internal virtual { } + ///////////////////////////////////////////////////////// + // USER OPERATION FAILED // + ///////////////////////////////////////////////////////// + // + // UserOpFailedHook: + // Data should be decoded as: + // + // bytes memory userOpData + // + + // _userOpFailedCall + // Details: + // userOpFailed/delegate = + // Inputs: User's calldata + // Function: Executing the function set by DAppControl + // Container: Inside of the FastLane ExecutionEnvironment + // Access: With storage access (read + write) only to the ExecutionEnvironment + // + // DApp exposure: Trustless + // User exposure: Trustless + function _userOpFailedCall(UserOperation calldata) internal virtual { } + ///////////////////////////////////////////////////////// // MEV ALLOCATION // ///////////////////////////////////////////////////////// diff --git a/src/contracts/dapp/DAppControl.sol b/src/contracts/dapp/DAppControl.sol index 8340bd7f4..dba8981ec 100644 --- a/src/contracts/dapp/DAppControl.sol +++ b/src/contracts/dapp/DAppControl.sol @@ -77,6 +77,17 @@ abstract contract DAppControl is DAppControlTemplate, ExecutionBase { return _preOpsCall(userOp); } + /// @notice The userOpFailed hook which is called if the UserOperation fails, before the metacall tx ends. + /// @param userOp The UserOperation struct. + function userOpFailedCall(UserOperation calldata userOp) + external + validControl + onlyAtlasEnvironment + onlyPhase(ExecutionPhase.UserOpFailed) + { + _userOpFailedCall(userOp); + } + /// @notice The preSolverCall hook which may be called before the SolverOperation is executed. /// @dev Should revert if any DApp-specific checks fail to indicate non-fulfillment. /// @param solverOp The SolverOperation to be executed after this hook has been called. diff --git a/src/contracts/interfaces/IDAppControl.sol b/src/contracts/interfaces/IDAppControl.sol index 20625d463..cc3b07e70 100644 --- a/src/contracts/interfaces/IDAppControl.sol +++ b/src/contracts/interfaces/IDAppControl.sol @@ -16,6 +16,8 @@ interface IDAppControl { function allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata data) external; + function userOpFailedCall(UserOperation calldata userOp) external; + function getDAppConfig(UserOperation calldata userOp) external view returns (DAppConfig memory dConfig); function getCallConfig() external view returns (CallConfig memory callConfig); diff --git a/src/contracts/interfaces/IExecutionEnvironment.sol b/src/contracts/interfaces/IExecutionEnvironment.sol index af0c7e040..bd57a6376 100644 --- a/src/contracts/interfaces/IExecutionEnvironment.sol +++ b/src/contracts/interfaces/IExecutionEnvironment.sol @@ -11,6 +11,8 @@ interface IExecutionEnvironment { function userWrapper(UserOperation calldata userOp) external payable returns (bytes memory userReturnData); + function userOpFailedWrapper(UserOperation calldata userOp) external; + function postOpsWrapper(bool solved, bytes calldata returnData) external; function solverPreTryCatch( diff --git a/src/contracts/types/AtlasErrors.sol b/src/contracts/types/AtlasErrors.sol index 606f7239f..dc37ddd3e 100644 --- a/src/contracts/types/AtlasErrors.sol +++ b/src/contracts/types/AtlasErrors.sol @@ -38,6 +38,7 @@ contract AtlasErrors { error SolverSimFail(uint256 solverOutcomeResult); // uint param is result returned in `verifySolverOp` error AllocateValueSimFail(); error PostOpsSimFail(); + error UserOpFailedHookSimFail(); error ValidCalls(ValidCallsResult); // Execution Environment @@ -51,6 +52,7 @@ contract AtlasErrors { error PostOpsDelegatecallFail(); error PostOpsDelegatecallReturnedFalse(); error AllocateValueDelegatecallFail(); + error UserOpFailedWrapperDelegatecallFail(); error NotEnvironmentOwner(); error ExecutionEnvironmentBalanceTooLow(); @@ -60,6 +62,7 @@ contract AtlasErrors { // error SolverFail(); // Only sim version of err is used error AllocateValueFail(); error PostOpsFail(); + error UserOpFailedHookFail(); error InvalidAccess(); // Escrow diff --git a/src/contracts/types/LockTypes.sol b/src/contracts/types/LockTypes.sol index 938b7199b..19841bc5e 100644 --- a/src/contracts/types/LockTypes.sol +++ b/src/contracts/types/LockTypes.sol @@ -25,5 +25,6 @@ enum ExecutionPhase { PostSolver, AllocateValue, PostOps, + UserOpFailed, FullyLocked }