Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: gas refund beneficiary #456

Merged
merged 9 commits into from
Nov 19, 2024
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
Loading