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

Refactor solver calls to directly from Atlas #225

Merged
merged 31 commits into from
Jun 18, 2024

Conversation

BenSparksCode
Copy link
Contributor

@BenSparksCode BenSparksCode commented Jun 12, 2024

Audit issues resolved in this PR:

Previously resolved audit issues affected by this PR:

Note: Atlas is currently about 800 bytes over the contract size limit. We will resolve the size limit issue after the major structural change PRs - most likely the upcoming locking mechanism PR and a gas refund accounting revamp PR.

thogard785 and others added 9 commits May 22, 2024 00:44
…three separate functions that are each called by escrow contract. This new design now has Atlas calling the solver contracts instead of the executionEnvironment calling the solver contracts, which should increase the safety. The math / bid verification accounting has also been simplified, and a new SolverTracker struct has been introduced to assist with data passing between the layers.
@BenSparksCode
Copy link
Contributor Author

Some clean up changes:

  • Made _bidFindingIteration() more "symmetrical" with _bidKnownIteration() by doing _executeSolverOperation() and allocateValue() directly inside. Also think its easier to see key.solverOutcome set to the solver index correctly in both that way.
  • Removed SolverTracker memory vars that weren't used
  • Renamed sTracker vars to solverTracker, a little bit more clear imo
  • Renamed sender param in atlasSolverCall() to solverOpFrom for clarity. This address param is always the solverOp.from.
  • Renamed bidRecipient param in atlasSolverCall() to executionEnvironment for clarity. Bids always sent to EE.
  • Added a few comments and natspec for clarity

Comment on lines 462 to 464
} else if (errorSwitch == SolverOperationReverted.selector) {
result = 1 << uint256(SolverOutcome.SolverOpReverted);
} else if (errorSwitch == PostSolverFailed.selector) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error and SolverOutcome names differ, suggest renaming the error:

-    error SolverOperationReverted();
+    error SolverOpReverted();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here 1632ec2

Comment on lines 468 to 470
} else if (errorSwitch == SolverBidUnpaid.selector) {
result = 1 << uint256(SolverOutcome.BidNotPaid);
} else if (errorSwitch == BalanceNotReconciled.selector) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error and SolverOutcome names differ, suggest renaming the error:

-    error SolverBidUnpaid();
+    error BidNotPaid();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here 1632ec2

Comment on lines 466 to 468
} else if (errorSwitch == IntentUnfulfilled.selector) {
result = 1 << uint256(SolverOutcome.IntentUnfulfilled);
} else if (errorSwitch == SolverBidUnpaid.selector) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IntentUnfulfilled does not seem to be thrown anywhere. Do we still need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, removed here 1632ec2

} else if (errorSwitch == InvalidEntry.selector) {
// DAppControl is attacking solver contract - treat as AlteredControl
result = 1 << uint256(SolverOutcome.AlteredControl);
} else if (errorSwitch == CallbackNotCalled.selector) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CallbackNotCalled does not seem to be thrown anywhere, do we still need it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I've split up the checks at end of solverCall() to revert with either BalanceNotReconciled or CallbackNotCalled. I think the granularity for diagnosing what exactly caused the failure is useful. See 1632ec2

@@ -85,6 +85,14 @@ contract Storage is AtlasEvents, AtlasErrors, AtlasConstants {
/// @return calledBack Boolean indicating whether the solver has called back via `reconcile`.
/// @return fulfilled Boolean indicating whether the solver's outstanding debt has been repaid via `reconcile`.
function solverLockData() public view returns (address currentSolver, bool calledBack, bool fulfilled) {
return _solverLockData();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the point of this, does it do something? Seems like this wrapping will increase the gas usage by about 88 from my tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its more gas efficient to call an internal function than a public one, but we still need to expose that function publically for dapps or solvers to call. But good catch, the outer function should be external not public. Fixed here c1e3e1b

test/Escrow.t.sol Outdated Show resolved Hide resolved
@@ -361,6 +330,7 @@ contract ExecutionEnvironmentTest is BaseTest {
(status,) = address(executionEnvironment).call(postOpsData);
}

/*
function test_solverMetaTryCatch() public {
Copy link
Contributor

@ephess ephess Jun 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't test_solverMetaTryCatch be removed now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but we should also add 2 new tests, for solverPreTryCatch and solverPostTryCatch. I've added a note to refactor this one into those 2 and left it commented as some of this test code may be reused in the new ones. Will add it to the list of tests to do after the audit fixes.

@ephess
Copy link
Contributor

ephess commented Jun 17, 2024

Aside from those comments above, this all LGTM.

From the issues above, they all seem to be addressed, except I'm not sure about https://github.com/spearbit-audits/review-fastlane/issues/149

That one feels like it's gonna need a thorough review.

endBalance = etherIsBidToken ? endBalance : address(this).balance;
}
// bidValue is not inverted; Higher bids are better; solver must deposit >= bidAmount
if (!solverTracker.invertsBidValue) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do the floor and ceiling records get taken after the pre and post solver calls for !invertsBidValue, but the records get taken beforehand for invertsBidValue?

Copy link
Contributor Author

@BenSparksCode BenSparksCode Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good question. So to rephrase, if mode is invertsBidValue, the change is measured over preSolverCall, solverCall, and postSolverCall. But if the mode is normal bid value, the change is just measured over solverCall.

I think it could be because, in invertsBidValue mode, the assets must be transferred to the solver in the preSolver hook (because it would be less secure / more complex with approvals, to allow the solver to pull them in the solver call). So we take the starting balance measurement before a dapp might transfer assets in the preSolver hook. Same reasoning on the other side I think - assets representing the bid may be transferred to the solver in postSolver hook so check end balance after that. This way we support transferring those bid assets on either side of the solver call.

Whereas in normal bid mode, we just measure the assets the solver pays which only happens in solver call. So we can measure that type of bid more tightly (excluding changes due to pre and post solver hooks).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you described would make sense, but that doesn't seem to be the implementation if i'm reading the code correctly. I worded it a bit weirdly at first. The below is the correct re-phrasing:

invertsBidValue the change is measured over preSolverCall and solverCall.
normal mode the change is measured over solverCall and postSolverCall.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to have a hook that takes place outside of the balance check for each option so that we can transform or otherwise adjust the balances. For the inverted bids, we wanted to be able to move inventory in so that we could then give it to the solver before the solver call, and for the normal bids we wanted to be able to move inventory out that we have received from the solver after the solver call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool makes sense, lets add comments to the code that document this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: This seems fine because the only party who can affect the bid amount calculation is the dapp, who in doing so knows they are affecting the bid this way. No other party can act between the solver execution and the end of the postSolverHook (end of bid phase calculation) other than the dapp.

Original: A consequence of this is that if a dapp calls contribute() in the postSolverHook in:

  • normal mode this causes the contribution to be reduced from the bid amount calculation
  • invertsBidBalue: this causes the contribution to be included in the bid amount calculation

Could this cause any issues?

@jgcogsystematictrading
Copy link
Contributor

This PR treats on-chain bid finding the same as regular known bids to determine solver payments. This seems intuitively right but Alex's comments seem to say we might want to do it some other way? https://github.com/spearbit-audits/review-fastlane/pull/1/files#r1561952304

@BenSparksCode
Copy link
Contributor Author

This PR treats on-chain bid finding the same as regular known bids to determine solver payments. This seems intuitively right but Alex's comments seem to say we might want to do it some other way? https://github.com/spearbit-audits/review-fastlane/pull/1/files#r1561952304

I think Alex's comment there is more accurately implemented in the code after this PR - any ETH surplus in the EE (change measured across the pre and post solver hooks) is only considered to be the bid, and is completely separate from the solver's gas or borrowed ETH repayment. The gas / borrow repayment is handled by solver calling reconcile() on Atlas during the solver call, and can be supplemented by the dapp calling contribute() on Atlas.

So I don't think there was any other way that was explored, but rather that the original idea has been implemented much more cleanly in this PR.

@BenSparksCode BenSparksCode merged commit f924c36 into spearbit-audit-fixes Jun 18, 2024
3 checks passed
@BenSparksCode BenSparksCode deleted the atlas-calls-solver branch June 18, 2024 07:38
(, calledback, success) = _solverLockData();

if (!calledback) revert CallbackNotCalled();
if (!success && deposits < claims + withdrawals) revert BalanceNotReconciled();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesnt this check still trust this unreliable fulfilled flag? https://github.com/orgs/FastLane-Labs/projects/7/views/1?pane=issue&itemId=67398082

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it still uses the fulfilled flag - but the idea is that it should be reliable now. As of the Lock PR, borrow is limited to SolverOperations phase or before, and reconcile() can only be called during the SolverOperations phase. So, if a solver calls reconcile() and sets fulfilled = true then we know it can't go back to unfulfilled as the dapp can no longer borrow - only repay further via contribute()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants