Skip to content

Commit

Permalink
Merge pull request #456 from FastLane-Labs/gas-refund-beneficiary
Browse files Browse the repository at this point in the history
feat: gas refund beneficiary
  • Loading branch information
BenSparksCode authored Nov 19, 2024
2 parents 66c1881 + 8127c01 commit 0ff49cc
Show file tree
Hide file tree
Showing 18 changed files with 147 additions and 52 deletions.
31 changes: 18 additions & 13 deletions src/contracts/atlas/Atlas.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ contract Atlas is Escrow, Factory {
/// @param userOp The UserOperation struct containing the user's transaction data.
/// @param solverOps The SolverOperation array containing the solvers' transaction data.
/// @param dAppOp The DAppOperation struct containing the DApp's transaction data.
/// @param gasRefundBeneficiary The address to receive the gas refund.
/// @return auctionWon A boolean indicating whether there was a successful, winning solver.
function metacall(
UserOperation calldata userOp, // set by user
SolverOperation[] calldata solverOps, // supplied by ops relay
DAppOperation calldata dAppOp // supplied by front end via atlas SDK
DAppOperation calldata dAppOp, // supplied by front end via atlas SDK
address gasRefundBeneficiary // address(0) = msg.sender
)
external
payable
Expand All @@ -70,21 +72,23 @@ contract Atlas is Escrow, Factory {

bool _isSimulation = msg.sender == SIMULATOR;
address _bundler = _isSimulation ? dAppOp.bundler : msg.sender;

(address _executionEnvironment, DAppConfig memory _dConfig) = _getOrCreateExecutionEnvironment(userOp);

ValidCallsResult _validCallsResult =
VERIFICATION.validateCalls(_dConfig, userOp, solverOps, dAppOp, msg.value, _bundler, _isSimulation);
if (_validCallsResult != ValidCallsResult.Valid) {
if (_isSimulation) revert VerificationSimFail(_validCallsResult);
{
ValidCallsResult _validCallsResult =
VERIFICATION.validateCalls(_dConfig, userOp, solverOps, dAppOp, msg.value, _bundler, _isSimulation);
if (_validCallsResult != ValidCallsResult.Valid) {
if (_isSimulation) revert VerificationSimFail(_validCallsResult);

// Gracefully return for results that need nonces to be stored and prevent replay attacks
if (uint8(_validCallsResult) >= _GRACEFUL_RETURN_THRESHOLD && !_dConfig.callConfig.allowsReuseUserOps()) {
return false;
}
// Gracefully return for results that need nonces to be stored and prevent replay attacks
if (uint8(_validCallsResult) >= _GRACEFUL_RETURN_THRESHOLD && !_dConfig.callConfig.allowsReuseUserOps())
{
return false;
}

// Revert for all other results
revert ValidCalls(_validCallsResult);
// Revert for all other results
revert ValidCalls(_validCallsResult);
}
}

// Initialize the environment lock and accounting values
Expand All @@ -97,7 +101,8 @@ contract Atlas is Escrow, Factory {
try this.execute(_dConfig, userOp, solverOps, _executionEnvironment, _bundler, dAppOp.userOpHash, _isSimulation)
returns (Context memory ctx) {
// Gas Refund to sender only if execution is successful
(uint256 _ethPaidToBundler, uint256 _netGasSurcharge) = _settle(ctx, _dConfig.solverGasLimit);
(uint256 _ethPaidToBundler, uint256 _netGasSurcharge) =
_settle(ctx, _dConfig.solverGasLimit, gasRefundBeneficiary);

auctionWon = ctx.solverSuccessful;
emit MetacallResult(
Expand Down
11 changes: 8 additions & 3 deletions src/contracts/atlas/GasAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -411,11 +411,13 @@ abstract contract GasAccounting is SafetyLocks {
/// refund for gas spent, and Atlas' gas surcharge is updated.
/// @param ctx Context struct containing relevant context information for the Atlas auction.
/// @param solverGasLimit The dApp's maximum gas limit for a solver, as set in the DAppConfig.
/// @param gasRefundBeneficiary The address to receive the gas refund.
/// @return claimsPaidToBundler The amount of ETH paid to the bundler in this function.
/// @return netAtlasGasSurcharge The net gas surcharge of the metacall, taken by Atlas.
function _settle(
Context memory ctx,
uint256 solverGasLimit
uint256 solverGasLimit,
address gasRefundBeneficiary
)
internal
returns (uint256 claimsPaidToBundler, uint256 netAtlasGasSurcharge)
Expand All @@ -426,6 +428,8 @@ abstract contract GasAccounting is SafetyLocks {
// If a solver won, their address is still in the _solverLock
(address _winningSolver,,) = _solverLockData();

if (gasRefundBeneficiary == address(0)) gasRefundBeneficiary = ctx.bundler;

// Load what we can from storage so that it shows up in the gasleft() calc

uint256 _claims;
Expand Down Expand Up @@ -455,8 +459,9 @@ abstract contract GasAccounting is SafetyLocks {
} else if (_winningSolver == ctx.bundler) {
claimsPaidToBundler = 0;
} else {
// this else block is only executed if there is no successful solver
claimsPaidToBundler = 0;
_winningSolver = ctx.bundler;
_winningSolver = gasRefundBeneficiary;
}

if (_amountSolverPays > _amountSolverReceives) {
Expand All @@ -477,7 +482,7 @@ abstract contract GasAccounting is SafetyLocks {
// Set lock to FullyLocked to prevent any reentrancy possibility
_setLockPhase(uint8(ExecutionPhase.FullyLocked));

if (claimsPaidToBundler != 0) SafeTransferLib.safeTransferETH(ctx.bundler, claimsPaidToBundler);
if (claimsPaidToBundler != 0) SafeTransferLib.safeTransferETH(gasRefundBeneficiary, claimsPaidToBundler);

return (claimsPaidToBundler, netAtlasGasSurcharge);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ contract FastLaneOnlineOuter is SolverGateway {

// Atlas call
(bool _success,) = ATLAS.call{ value: msg.value, gas: _metacallGasLimit(_gasReserved, userOp.gas, gasleft()) }(
abi.encodeCall(IAtlas.metacall, (userOp, _solverOps, _dAppOp))
abi.encodeCall(IAtlas.metacall, (userOp, _solverOps, _dAppOp, address(0)))
);

// Revert if the metacall failed - neither solvers nor baseline call fulfilled swap intent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ contract GeneralizedBackrunUserBundler is DAppControl {
SolverOperation[] memory _solverOps = _getSolverOps(solverOpHashes);

(bool _success, bytes memory _data) =
ATLAS.call{ value: msg.value }(abi.encodeCall(IAtlas.metacall, (userOp, _solverOps, _dAppOp)));
ATLAS.call{ value: msg.value }(abi.encodeCall(IAtlas.metacall, (userOp, _solverOps, _dAppOp, address(0))));
if (!_success) {
assembly {
revert(add(_data, 32), mload(_data))
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/helpers/Simulator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ contract Simulator is AtlasErrors {
payable
{
if (msg.sender != address(this)) revert InvalidEntryFunction();
if (!IAtlas(atlas).metacall{ value: msg.value }(userOp, solverOps, dAppOp)) {
if (!IAtlas(atlas).metacall{ value: msg.value }(userOp, solverOps, dAppOp, address(0))) {
revert NoAuctionWinner(); // should be unreachable
}
revert SimulationPassed();
Expand Down
3 changes: 2 additions & 1 deletion src/contracts/interfaces/IAtlas.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ interface IAtlas {
function metacall(
UserOperation calldata userOp,
SolverOperation[] calldata solverOps,
DAppOperation calldata dAppOp
DAppOperation calldata dAppOp,
address gasRefundBeneficiary
)
external
payable
Expand Down
4 changes: 2 additions & 2 deletions test/Accounting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ contract AccountingTest is BaseTest {
SolverOperation[] memory solverOps = _setupBorrowRepayTestUsingBasicSwapIntent(address(honestSolver));

vm.startPrank(userEOA);
atlas.metacall{ value: 0 }({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp });
atlas.metacall{ value: 0 }({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp, gasRefundBeneficiary: address(0) });
vm.stopPrank();

// console.log("\nAFTER METACALL");
Expand All @@ -90,7 +90,7 @@ contract AccountingTest is BaseTest {
SolverOperation[] memory solverOps = _setupBorrowRepayTestUsingBasicSwapIntent(address(evilSolver));

vm.startPrank(userEOA);
atlas.metacall{ value: 0 }({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp });
atlas.metacall{ value: 0 }({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp, gasRefundBeneficiary: address(0) });
vm.stopPrank();
}

Expand Down
10 changes: 5 additions & 5 deletions test/Escrow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ contract EscrowTest is BaseTest {
DAppOperation memory dappOp = validDAppOperation(userOp, solverOps).build();

vm.prank(userEOA);
bool auctionWon = atlas.metacall(userOp, solverOps, dappOp);
bool auctionWon = atlas.metacall(userOp, solverOps, dappOp, address(0));

assertLe(dAppControl.userOpGasLeft(), userGasLim, "userOpGasLeft should be <= userGasLim");
assertTrue(auctionWon, "2nd auction should have been won");
Expand Down Expand Up @@ -239,7 +239,7 @@ contract EscrowTest is BaseTest {
// Send msg.value so it must be sent back, testing the upper bound of remaining gas for graceful return
deal(userEOA, 1 ether);
vm.prank(userEOA);
bool auctionWon = atlas.metacall{gas: metacallGasLim, value: 1 ether}(userOp, solverOps, dappOp);
bool auctionWon = atlas.metacall{gas: metacallGasLim, value: 1 ether}(userOp, solverOps, dappOp, address(0));
assertEq(auctionWon, false, "call should not revert but auction should not be won either");
}

Expand Down Expand Up @@ -332,7 +332,7 @@ contract EscrowTest is BaseTest {
}

vm.prank(userEOA);
bool auctionWon = atlas.metacall(userOp, solverOps, dappOp);
bool auctionWon = atlas.metacall(userOp, solverOps, dappOp, address(0));

if (!revertExpected) {
assertTrue(auctionWon, "auction should have been won");
Expand Down Expand Up @@ -558,7 +558,7 @@ contract EscrowTest is BaseTest {
DAppOperation memory dappOp = validDAppOperation(userOp, solverOps).build();

vm.prank(userEOA);
(bool success,) = address(atlas).call(abi.encodeCall(atlas.metacall, (userOp, solverOps, dappOp)));
(bool success,) = address(atlas).call(abi.encodeCall(atlas.metacall, (userOp, solverOps, dappOp, address(0))));
assertTrue(success, "metacall should have succeeded");
}

Expand Down Expand Up @@ -670,7 +670,7 @@ contract EscrowTest is BaseTest {

vm.prank(userEOA);
if (metacallShouldRevert) vm.expectRevert(); // Metacall should revert, the reason isn't important, we're only checking the event
atlas.metacall(userOp, solverOps, dappOp);
atlas.metacall(userOp, solverOps, dappOp, address(0));
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/ExPost.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ contract ExPostTest is BaseTest {
// uint256 solverTwoAtlEthBalance = atlas.balanceOf(solverTwoEOA);

(bool success,) =
address(atlas).call(abi.encodeCall(atlas.metacall, (userOp, solverOps, dAppOp)));
address(atlas).call(abi.encodeCall(atlas.metacall, (userOp, solverOps, dAppOp, address(0))));

if (success) {
console.log("success!");
Expand Down
6 changes: 3 additions & 3 deletions test/FlashLoan.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ contract FlashLoanTest is BaseTest {
result
);
vm.expectRevert();
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp });
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp, gasRefundBeneficiary: address(0) });
vm.stopPrank();

// now try it again with a valid solverOp - but dont fully pay back
Expand Down Expand Up @@ -181,7 +181,7 @@ contract FlashLoanTest is BaseTest {
result
);
vm.expectRevert();
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp });
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp, gasRefundBeneficiary: address(0) });
vm.stopPrank();

// final try, should be successful with full payback
Expand Down Expand Up @@ -246,7 +246,7 @@ contract FlashLoanTest is BaseTest {
true,
result
);
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp });
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp, gasRefundBeneficiary: address(0) });
vm.stopPrank();

// atlas 2e beginning bal + 1e from solver +100e eth from user = 103e atlas total
Expand Down
2 changes: 1 addition & 1 deletion test/GasAccounting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ contract MockGasAccounting is TestAtlas, BaseTest {
}

function settle(Context memory ctx) external returns (uint256, uint256) {
return _settle(ctx, MOCK_SOLVER_GAS_LIMIT);
return _settle(ctx, MOCK_SOLVER_GAS_LIMIT, makeAddr("bundler"));
}

function adjustAccountingForFees(Context memory ctx)
Expand Down
93 changes: 91 additions & 2 deletions test/MainTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -404,15 +404,15 @@ contract MainTest is BaseTest {

vm.startPrank(userEOA);
IERC20(TOKEN_ONE).approve(address(atlas), type(uint256).max);
atlas.metacall(userOp, solverOps, dAppOp);
atlas.metacall(userOp, solverOps, dAppOp, address(0));
vm.stopPrank();

// Execution environment should exist now
(,, exists) = atlas.getExecutionEnvironment(userEOA, address(v2DAppControl));
assertTrue(exists, "ExecutionEnvironment wasn't created");
}

function testTestUserOperation_SkipCoverage() public {
function testUserOperation_SkipCoverage() public {
uint8 v;
bytes32 r;
bytes32 s;
Expand Down Expand Up @@ -494,4 +494,93 @@ contract MainTest is BaseTest {
assertFalse(abi.decode(data, (bool)), "Failure case did not return false");
vm.stopPrank();
}

function testGasRefundBeneficiarySolverSucceeds() public {
uint8 v;
bytes32 r;
bytes32 s;

address beneficiary = makeAddr("beneficiary");

vm.prank(solverOneEOA);
atlas.bond(1 ether);

UserOperation memory userOp = helper.buildUserOperation(POOL_ONE, POOL_TWO, userEOA, TOKEN_ONE);
// User does not sign their own operation when bundling

SolverOperation[] memory solverOps = new SolverOperation[](1);
bytes memory solverOpData = helper.buildV2SolverOperationData(POOL_TWO, POOL_ONE);
solverOps[0] = helper.buildSolverOperation(userOp, solverOpData, solverOneEOA, address(solverOne), 2e17, 0);
(v, r, s) = vm.sign(solverOnePK, atlasVerification.getSolverPayload(solverOps[0]));
solverOps[0].signature = abi.encodePacked(r, s, v);

DAppOperation memory dAppOp = helper.buildDAppOperation(governanceEOA, userOp, solverOps);
(v, r, s) = vm.sign(governancePK, atlasVerification.getDAppOperationPayload(dAppOp));
dAppOp.signature = abi.encodePacked(r, s, v);

uint256 bondedBalanceBefore = atlas.balanceOfBonded(beneficiary);

console.log("Beneficiary's balance before metacall", beneficiary.balance);
console.log("Beneficiary's AtlETH bonded balance before metacall", bondedBalanceBefore);

assertEq(beneficiary.balance, 0, "Beneficiary should start with 0 balance");
assertEq(bondedBalanceBefore, 0, "Beneficiary's AtlETH bonded balance should start with 0");

vm.startPrank(userEOA);
IERC20(TOKEN_ONE).approve(address(atlas), type(uint256).max);
atlas.metacall(userOp, solverOps, dAppOp, beneficiary);
vm.stopPrank();

uint256 bondedBalanceAfter = atlas.balanceOfBonded(beneficiary);

console.log("Beneficiary's balance after metacall", beneficiary.balance);
console.log("Beneficiary's AtlETH bonded balance after metacall", bondedBalanceAfter);

assertGt(beneficiary.balance, 0, "Beneficiary should have received some ETH");
assertEq(bondedBalanceAfter, 0, "Beneficiary's AtlETH bonded balance should still be 0");
}

function testGasRefundBeneficiarySolverFails() public {
uint8 v;
bytes32 r;
bytes32 s;

address beneficiary = makeAddr("beneficiary");

vm.prank(solverOneEOA);
atlas.bond(1 ether);

UserOperation memory userOp = helper.buildUserOperation(POOL_ONE, POOL_TWO, userEOA, TOKEN_ONE);
// User does not sign their own operation when bundling

SolverOperation[] memory solverOps = new SolverOperation[](1);
bytes memory solverOpData = helper.buildV2SolverOperationData(POOL_ONE, POOL_ONE); // will fail
solverOps[0] = helper.buildSolverOperation(userOp, solverOpData, solverOneEOA, address(solverOne), 2e17, 0);
(v, r, s) = vm.sign(solverOnePK, atlasVerification.getSolverPayload(solverOps[0]));
solverOps[0].signature = abi.encodePacked(r, s, v);

DAppOperation memory dAppOp = helper.buildDAppOperation(governanceEOA, userOp, solverOps);
(v, r, s) = vm.sign(governancePK, atlasVerification.getDAppOperationPayload(dAppOp));
dAppOp.signature = abi.encodePacked(r, s, v);

uint256 bondedBalanceBefore = atlas.balanceOfBonded(beneficiary);

console.log("Beneficiary's balance before metacall", beneficiary.balance);
console.log("Beneficiary's AtlETH bonded balance before metacall", bondedBalanceBefore);

assertEq(beneficiary.balance, 0, "Beneficiary should start with 0 balance");
assertEq(bondedBalanceBefore, 0, "Beneficiary's AtlETH bonded balance should start with 0");

vm.startPrank(userEOA);
IERC20(TOKEN_ONE).approve(address(atlas), type(uint256).max);
atlas.metacall(userOp, solverOps, dAppOp, beneficiary);
vm.stopPrank();

uint256 bondedBalanceAfter = atlas.balanceOfBonded(beneficiary);
console.log("Beneficiary's balance after metacall", beneficiary.balance);
console.log("Beneficiary's AtlETH bonded balance after metacall", bondedBalanceAfter);

assertEq(beneficiary.balance, 0, "Beneficiary should still have 0 balance");
assertGt(bondedBalanceAfter, 0, "Beneficiary's AtlETH bonded balance should be greater than 0");
}
}
4 changes: 2 additions & 2 deletions test/OEV.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ contract OEVTest is BaseTest {

// Should Fail
vm.prank(userEOA);
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp });
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp, gasRefundBeneficiary: address(0) });

assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), uint(AggregatorV2V3Interface(chainlinkETHUSD).latestAnswer()), "Metacall unexpectedly succeeded");

Expand All @@ -176,7 +176,7 @@ contract OEVTest is BaseTest {

// Should Succeed
vm.prank(userEOA);
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp });
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp, gasRefundBeneficiary: address(0) });

console.log("Metacall Gas Cost:", gasLeftBefore - gasleft());

Expand Down
2 changes: 1 addition & 1 deletion test/OEValt.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ contract OEVTest is BaseTest {

// Should Succeed
vm.prank(transmitter);
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp });
atlas.metacall({ userOp: userOp, solverOps: solverOps, dAppOp: dAppOp, gasRefundBeneficiary: address(0) });

assertEq(uint(chainlinkAtlasWrapper.latestAnswer()), targetOracleAnswer, "Wrapper did not update as expected");
assertTrue(uint(chainlinkAtlasWrapper.latestAnswer()) != uint(IChainlinkFeed(chainlinkETHUSD).latestAnswer()), "Wrapper and base feed should report different answers");
Expand Down
Loading

0 comments on commit 0ff49cc

Please sign in to comment.