Skip to content

Commit

Permalink
Merge pull request #427 from FastLane-Labs/solver-surcharge-tracking
Browse files Browse the repository at this point in the history
feat: full solver failure surcharge solution
  • Loading branch information
BenSparksCode authored Oct 17, 2024
2 parents ae20f83 + 771f801 commit 54a6c98
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 46 deletions.
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 2 files
+20 −0 src/Vm.sol
+1 −1 test/Vm.t.sol
56 changes: 49 additions & 7 deletions src/contracts/atlas/GasAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ abstract contract GasAccounting is SafetyLocks {
// Atlas surcharge is based on the raw claims value.
_setFees(_rawClaims.getSurcharge(ATLAS_SURCHARGE_RATE));
_setDeposits(msg.value);
_setSolverSurcharge(0);

// Explicitly set writeoffs and withdrawals to 0 in case multiple metacalls in single tx.
_setWriteoffs(0);
Expand Down Expand Up @@ -295,7 +296,19 @@ abstract contract GasAccounting is SafetyLocks {
} else {
// CASE: Solver failed, so we calculate what they owe.
uint256 _gasUsedWithSurcharges = _gasUsed.withSurcharges(ATLAS_SURCHARGE_RATE, BUNDLER_SURCHARGE_RATE);
_assign(solverOp.from, _gasUsedWithSurcharges, _gasUsedWithSurcharges, false);
uint256 _surchargesOnly = _gasUsedWithSurcharges - _gasUsed;

// In `_assign()`, the failing solver's bonded AtlETH balance is reduced by `_gasUsedWithSurcharges`. Any
// deficit from that operation is added to `writeoffs` and returned as `_assignDeficit` below. The portion
// that can be covered by the solver's AtlETH is added to `deposits`, to account that it has been paid.
uint256 _assignDeficit = _assign(solverOp.from, _gasUsedWithSurcharges, _gasUsedWithSurcharges, false);

// We track the surcharges (in excess of deficit - so the actual AtlETH that can be collected) separately,
// so that in the event of no successful solvers, any `_assign()`ed surcharges can be attributed to an
// increase in Atlas' cumulative surcharge.
if (_surchargesOnly > _assignDeficit) {
_setSolverSurcharge(solverSurcharge() + (_surchargesOnly - _assignDeficit));
}
}
}

Expand Down Expand Up @@ -329,24 +342,51 @@ abstract contract GasAccounting is SafetyLocks {
)
{
uint256 _surcharge = S_cumulativeSurcharge;
uint256 _fees = fees();

adjustedWithdrawals = withdrawals();
adjustedDeposits = deposits();
adjustedClaims = claims();
adjustedWriteoffs = writeoffs();
uint256 _fees = fees();

uint256 _gasLeft = gasleft(); // Hold this constant for the calculations

// Estimate the unspent, remaining gas that the Solver will not be liable for.
uint256 _gasRemainder = _gasLeft * tx.gasprice;

// Calculate the preadjusted netAtlasGasSurcharge
netAtlasGasSurcharge = _fees - _gasRemainder.getSurcharge(ATLAS_SURCHARGE_RATE);

adjustedClaims -= _gasRemainder.withSurcharge(BUNDLER_SURCHARGE_RATE);
adjustedWithdrawals += netAtlasGasSurcharge;
S_cumulativeSurcharge = _surcharge + netAtlasGasSurcharge; // Update the cumulative surcharge

if (ctx.solverSuccessful) {
// If a solver was successful, calc the full Atlas gas surcharge on the gas cost of the entire metacall, and
// add it to withdrawals so that the cost is assigned to winning solver by the end of _settle(). This will
// be offset by any gas surcharge paid by failed solvers, which would have been added to deposits or
// writeoffs in _handleSolverAccounting(). As such, the winning solver does not pay for surcharge on the gas
// used by other solvers.
netAtlasGasSurcharge = _fees - _gasRemainder.getSurcharge(ATLAS_SURCHARGE_RATE);
adjustedWithdrawals += netAtlasGasSurcharge;
S_cumulativeSurcharge = _surcharge + netAtlasGasSurcharge;
} else {
// If no successful solvers, only collect partial surcharges from solver's fault failures (if any)
uint256 _solverSurcharge = solverSurcharge();
if (_solverSurcharge > 0) {
netAtlasGasSurcharge = _solverSurcharge.getPortionFromTotalSurcharge({
targetSurchargeRate: ATLAS_SURCHARGE_RATE,
totalSurchargeRate: ATLAS_SURCHARGE_RATE + BUNDLER_SURCHARGE_RATE
});

// When no winning solvers, bundler max refund is 80% of metacall gas cost. The remaining 20% can be
// collected through storage refunds. Any excess bundler surcharge is instead taken as Atlas surcharge.
uint256 _bundlerSurcharge = _solverSurcharge - netAtlasGasSurcharge;
uint256 _maxBundlerRefund = adjustedClaims.withoutSurcharge(BUNDLER_SURCHARGE_RATE).maxBundlerRefund();
if (_bundlerSurcharge > _maxBundlerRefund) {
netAtlasGasSurcharge += _bundlerSurcharge - _maxBundlerRefund;
}

adjustedWithdrawals += netAtlasGasSurcharge;
S_cumulativeSurcharge = _surcharge + netAtlasGasSurcharge;
}
return (adjustedWithdrawals, adjustedDeposits, adjustedClaims, adjustedWriteoffs, netAtlasGasSurcharge);
}

// Calculate whether or not the bundler used an excessive amount of gas and, if so, reduce their
// gas rebate. By reducing the claims, solvers end up paying less in total.
Expand Down Expand Up @@ -416,6 +456,8 @@ abstract contract GasAccounting is SafetyLocks {
if (ctx.solverSuccessful && _winningSolver != ctx.bundler) {
_amountSolverPays += _adjustedClaims;
claimsPaidToBundler = _adjustedClaims;
} else if (_winningSolver == ctx.bundler) {
claimsPaidToBundler = 0;
} else {
claimsPaidToBundler = 0;
_winningSolver = ctx.bundler;
Expand Down
12 changes: 12 additions & 0 deletions src/contracts/atlas/Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ contract Storage is AtlasEvents, AtlasErrors, AtlasConstants {
bytes32 private constant _T_LOCK_SLOT = keccak256("ATLAS_LOCK");
bytes32 private constant _T_SOLVER_LOCK_SLOT = keccak256("ATLAS_SOLVER_LOCK");
bytes32 private constant _T_SOLVER_TO_SLOT = keccak256("ATLAS_SOLVER_TO");

bytes32 private constant _T_CLAIMS_SLOT = keccak256("ATLAS_CLAIMS");
bytes32 private constant _T_FEES_SLOT = keccak256("ATLAS_FEES");
bytes32 private constant _T_WRITEOFFS_SLOT = keccak256("ATLAS_WRITEOFFS");
bytes32 private constant _T_WITHDRAWALS_SLOT = keccak256("ATLAS_WITHDRAWALS");
bytes32 private constant _T_DEPOSITS_SLOT = keccak256("ATLAS_DEPOSITS");
bytes32 private constant _T_SOLVER_SURCHARGE_SLOT = keccak256("ATLAS_SOLVER_SURCHARGE");

// AtlETH storage
uint256 internal S_totalSupply;
Expand Down Expand Up @@ -173,6 +175,10 @@ contract Storage is AtlasEvents, AtlasErrors, AtlasConstants {
return uint256(_tload(_T_DEPOSITS_SLOT));
}

function solverSurcharge() internal view returns (uint256) {
return uint256(_tload(_T_SOLVER_SURCHARGE_SLOT));
}

function _lock() internal view returns (address activeEnvironment, uint32 callConfig, uint8 phase) {
bytes32 _lockData = _tload(_T_LOCK_SLOT);
activeEnvironment = address(uint160(uint256(_lockData >> 40)));
Expand Down Expand Up @@ -266,6 +272,12 @@ contract Storage is AtlasEvents, AtlasErrors, AtlasConstants {
_tstore(_T_DEPOSITS_SLOT, bytes32(newDeposits));
}

// NOTE: Only captures surcharges for failed solver Ops where
// solver is at fault
function _setSolverSurcharge(uint256 newSurcharge) internal {
_tstore(_T_SOLVER_SURCHARGE_SLOT, bytes32(newSurcharge));
}

// ------------------------------------------------------ //
// Transient Storage Helpers //
// ------------------------------------------------------ //
Expand Down
20 changes: 19 additions & 1 deletion src/contracts/libraries/AccountingMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity 0.8.25;

library AccountingMath {
// Gas Accounting public constants
uint256 internal constant _MAX_BUNDLER_REFUND_RATE = 8_000_000; // out of 10_000_000 = 80%
uint256 internal constant _SOLVER_GAS_LIMIT_BUFFER_PERCENTAGE = 500_000; // out of 10_000_000 = 5%
uint256 internal constant _SCALE = 10_000_000; // 10_000_000 / 10_000_000 = 100%
uint256 internal constant _FIXED_GAS_OFFSET = 85_000;
Expand Down Expand Up @@ -39,6 +39,24 @@ library AccountingMath {
surchargeAmount = unadjustedAmount * surchargeRate / _SCALE;
}

function getPortionFromTotalSurcharge(
uint256 totalSurcharge,
uint256 targetSurchargeRate,
uint256 totalSurchargeRate
)
internal
pure
returns (uint256 surchargePortion)
{
surchargePortion = totalSurcharge * targetSurchargeRate / totalSurchargeRate;
}

// NOTE: This max should only be applied when there are no winning solvers.
// Set to 80% of the metacall gas cost, because the remaining 20% can be collected through storage refunds.
function maxBundlerRefund(uint256 metacallGasCost) internal pure returns (uint256 maxRefund) {
maxRefund = metacallGasCost * _MAX_BUNDLER_REFUND_RATE / _SCALE;
}

function solverGasLimitScaledDown(
uint256 solverOpGasLimit,
uint256 dConfigGasLimit
Expand Down
4 changes: 2 additions & 2 deletions test/Escrow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ contract EscrowTest is BaseTest {
.withGas(solverGasLimit)
.signAndBuild(address(atlasVerification), solverOnePK);

executeSolverOperationCase(userOp, solverOps, false, false, 1 << uint256(SolverOutcome.InsufficientEscrow), true);
executeSolverOperationCase(userOp, solverOps, false, false, 1 << uint256(SolverOutcome.InsufficientEscrow), false);
}

function test_executeSolverOperation_validateSolverOperation_callValueTooHigh_SkipCoverage() public {
Expand All @@ -370,7 +370,7 @@ contract EscrowTest is BaseTest {
function test_executeSolverOperation_validateSolverOperation_userOutOfGas_SkipCoverage() public {
(UserOperation memory userOp, SolverOperation[] memory solverOps) = executeSolverOperationInit(defaultCallConfig().build());
this.executeSolverOperationCase{gas: _VALIDATION_GAS_LIMIT + _SOLVER_GAS_LIMIT + 1_000_000}(
userOp, solverOps, false, false, 1 << uint256(SolverOutcome.UserOutOfGas), true
userOp, solverOps, false, false, 1 << uint256(SolverOutcome.UserOutOfGas), false
);
}

Expand Down
39 changes: 24 additions & 15 deletions test/FLOnline.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ contract FastLaneOnlineTest is BaseTest {
bool solverFour;
}

uint256 constant ERR_MARGIN = 0.15e18; // 15% error margin
// Only Atlas surcharge kept if all fail, bundler surcharge paid to bundler
uint256 constant SURCHARGE_PER_SOLVER_IF_ALL_FAIL = 14_000e9; // 14k Gwei (avg, differs for ERC20/native in/out)
uint256 constant ERR_MARGIN = 0.22e18; // 22% error margin
address internal constant NATIVE_TOKEN = address(0);

address protocolGuildWallet = 0x25941dC771bB64514Fc8abBce970307Fb9d477e9;
Expand All @@ -60,7 +62,6 @@ contract FastLaneOnlineTest is BaseTest {

uint256 goodSolverBidETH = 1.2 ether; // more than baseline swap amountOut if tokenOut is WETH/ETH
uint256 goodSolverBidDAI = 3100e18; // more than baseline swap amountOut if tokenOut is DAI
uint256 defaultMsgValue = 1e16; // 0.01 ETH for bundler gas, treated as donation
uint256 defaultGasLimit = 2_000_000;
uint256 defaultGasPrice;
uint256 defaultDeadlineBlock;
Expand Down Expand Up @@ -1279,7 +1280,6 @@ contract FastLaneOnlineTest is BaseTest {
internal
{
bool nativeTokenIn = args.swapIntent.tokenUserSells == NATIVE_TOKEN;
bool nativeTokenOut = args.swapIntent.tokenUserBuys == NATIVE_TOKEN;
bool solverWon = winningSolver != address(0);

beforeVars.userTokenOutBalance = _balanceOf(args.swapIntent.tokenUserBuys, userEOA);
Expand All @@ -1291,9 +1291,6 @@ contract FastLaneOnlineTest is BaseTest {
beforeVars.solverTwoRep = flOnline.solverReputation(solverTwoEOA);
beforeVars.solverThreeRep = flOnline.solverReputation(solverThreeEOA);

// adjust userTokenInBalance if native token - exclude gas treated as donation
if (nativeTokenIn) beforeVars.userTokenInBalance -= defaultMsgValue;

uint256 txGasUsed;
uint256 estAtlasGasSurcharge = gasleft(); // Reused below during calculations

Expand All @@ -1315,13 +1312,26 @@ contract FastLaneOnlineTest is BaseTest {
// Return early if transaction expected to revert. Balance checks below would otherwise fail.
if (!swapCallShouldSucceed) return;

// Check Atlas gas surcharge earned is within 15% of the estimated gas surcharge
assertApproxEqRel(
atlas.cumulativeSurcharge() - beforeVars.atlasGasSurcharge,
estAtlasGasSurcharge,
ERR_MARGIN,
"Atlas gas surcharge not within estimated range"
);
if (solverCount == 0) {
// If zero solvers, no surcharge taken
assertEq(atlas.cumulativeSurcharge(), beforeVars.atlasGasSurcharge, "Atlas gas surcharge should not change");
} else if (solverWon) {
// Check Atlas gas surcharge earned is within 15% of the estimated gas surcharge
assertApproxEqRel(
atlas.cumulativeSurcharge() - beforeVars.atlasGasSurcharge,
estAtlasGasSurcharge,
ERR_MARGIN,
"Atlas gas surcharge not within estimated range (solver won)"
);
} else {
// If all solvers fail, surcharge taken only on gas cost of solverOps failed due to solver fault
assertApproxEqRel(
atlas.cumulativeSurcharge() - beforeVars.atlasGasSurcharge,
SURCHARGE_PER_SOLVER_IF_ALL_FAIL * solverCount,
ERR_MARGIN,
"Atlas gas surcharge not within estimated range (solvers failed)"
);
}

// Check user's balances changed as expected
assertTrue(
Expand Down Expand Up @@ -1446,10 +1456,9 @@ contract FastLaneOnlineTest is BaseTest {
newArgs.deadline = defaultDeadlineBlock;
newArgs.gas = defaultGasLimit;
newArgs.maxFeePerGas = defaultGasPrice;
newArgs.msgValue = defaultMsgValue;

// Add amountUserSells of ETH to the msg.value of the fastOnlineSwap call
if (nativeTokenIn) newArgs.msgValue += swapIntent.amountUserSells;
if (nativeTokenIn) newArgs.msgValue = swapIntent.amountUserSells;
}

function _buildBaselineCall(
Expand Down
10 changes: 0 additions & 10 deletions test/GasAccounting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import { BaseTest } from "test/base/BaseTest.t.sol";
contract MockGasAccounting is TestAtlas, BaseTest {
uint256 public constant MOCK_SOLVER_GAS_LIMIT = 500_000;


constructor(
uint256 _escrowDuration,
uint256 _atlasSurchargeRate,
Expand Down Expand Up @@ -1026,15 +1025,6 @@ contract GasAccountingTest is AtlasConstants, BaseTest {
assertEq(unbonding, unbondingBefore);
}

function test_settle_withFailedsolver_reverts() public {
// Setup context with initial claims and deposits
Context memory ctx = setupContext(1 ether, 1 ether, 4000 ether, 1000 ether, false);

// Expect a revert due to insufficient total balance for a failed solver
vm.expectRevert();
mockGasAccounting.settle(ctx);
}

function test_settle_with_deposits() public {
Context memory ctx = setupContext(1 ether, 0.5 ether, 4000 ether, 1000 ether, true);
// Check initial balances
Expand Down
33 changes: 23 additions & 10 deletions test/TrebleSwap.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ contract TrebleSwapTest is BaseTest {
address BRETT = 0x532f27101965dd16442E59d40670FaF5eBB142E4;
address TREB; // will be set to value in DAppControl in setUp

uint256 ERR_MARGIN = 0.18e18; // 18% error margin
uint256 ERR_MARGIN = 0.22e18; // 22% error margin
uint256 bundlerGasEth = 1e16;

TrebleSwapDAppControl trebleSwapControl;
Expand Down Expand Up @@ -288,9 +288,7 @@ contract TrebleSwapTest is BaseTest {
beforeVars.solverTrebBalance = _balanceOf(address(TREB), winningSolver);
beforeVars.burnAddressTrebBalance = _balanceOf(address(TREB), BURN);
beforeVars.atlasGasSurcharge = atlas.cumulativeSurcharge();
uint256 msgValue = (args.nativeInput ? swapInfo.inputAmount : 0) + bundlerGasEth;
if (args.nativeInput) beforeVars.userInputTokenBalance -= bundlerGasEth;
if (args.nativeOutput) beforeVars.userOutputTokenBalance -= bundlerGasEth;
uint256 msgValue = args.nativeInput ? swapInfo.inputAmount : 0;

uint256 txGasUsed;
uint256 estAtlasGasSurcharge = gasleft(); // Reused below during calculations
Expand All @@ -306,13 +304,28 @@ contract TrebleSwapTest is BaseTest {
// Check Atlas auctionWon return value
assertEq(auctionWon, auctionWonExpected, "auctionWon not as expected");

// Check msg.value is 0 unless sending ETH as the input token to be swapped
if (!args.nativeInput) assertEq(msgValue, 0, "msgValue should have been 0");

// Check Atlas gas surcharge change
assertApproxEqRel(
atlas.cumulativeSurcharge() - beforeVars.atlasGasSurcharge,
estAtlasGasSurcharge,
ERR_MARGIN,
"Atlas gas surcharge not within estimated range"
);
if (args.solverOps.length > 0 && auctionWonExpected) {
assertApproxEqRel(
atlas.cumulativeSurcharge() - beforeVars.atlasGasSurcharge,
estAtlasGasSurcharge,
ERR_MARGIN,
"Atlas gas surcharge not within estimated range"
);
} else if (args.solverOps.length == 0) {
// No surcharge taken if no solvers.
assertEq(
atlas.cumulativeSurcharge(),
beforeVars.atlasGasSurcharge,
"Atlas gas surcharge changed when zero solvers"
);
} else {
// If solver failed (solver's fault), surcharge still taken, but only on failing solverOp portion. Difficult
// to estimate what that would be so skip this check in that 1 test case.
}

// Check user input token change
if (args.nativeInput && auctionWonExpected) {
Expand Down

0 comments on commit 54a6c98

Please sign in to comment.