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

BM-476: Callbacks #335

Merged
merged 60 commits into from
Mar 5, 2025
Merged

BM-476: Callbacks #335

merged 60 commits into from
Mar 5, 2025

Conversation

willpote
Copy link
Contributor

  • Callbacks are only ever executed once, the first time a request is fulfilled
  • Add a Callback interface and base contract for apps.
  • In the non-locked path, callbacks are stored along with the price, when the request is "priced"

@willpote
Copy link
Contributor Author

Thinking about this a bit more, the fact that the requestor can control the contents of the callback, specifically the journal, feels somewhat risky to me. I can't think of an immediate attack against the current construction, but it feels somewhat close in nature to a attack I have seen against a contract that made user-specified calls and the attacker used this to call transferFrom and steal funds that had been approved to the contract.

It's very possible this is fine, but I do have two ideas for mitigations.

  • Pass the callbacks through a proxy that is intended to be untrusted (i.e. no one should ever provide is an ERC20 approval or grant it a privileged role in any way). The market contract itself will only ever call the proxy.
  • Remove the journal from the directly sent data and instead stash it in transient storage. A call from the called contract can get journal's digest or contents. This has a few challenges though, including storing arbitrary length byte strings in transient storage.

Not sure what the right level of caution is here, or the right mitigation if caution is warranted.

Is this not a problem with the image that the callback implementer has chosen to use? If it is possible for the image to generate a journal that leads to tokens being transferred to the wrong person, then the image is the problem?

One thing this did make me think of is, would make sense to pass in the Predicate also used for the journal into the callback? Then even for more general purpose images, the callback can verify the journal meets some requirements? [I guess this would require us making the assessor output the predicate]

Re: the two mitigations, I don't fully see how they help. Even with a proxy, or fetching the journal from storage, ultimately there is a contract that needs to read the journal and decide what to do as a result of it- and if the journal can be manipulated, that contract will do the wrong thing.

@willpote
Copy link
Contributor Author

Another thought, it probably makes sense for the callback template to have some functionality built into it around checking the image id is as expected, as that does feel important. Will add something

@nategraf
Copy link
Member

Thinking about this a bit more, the fact that the requestor can control the contents of the callback, specifically the journal, feels somewhat risky to me. I can't think of an immediate attack against the current construction, but it feels somewhat close in nature to a attack I have seen against a contract that made user-specified calls and the attacker used this to call transferFrom and steal funds that had been approved to the contract.
It's very possible this is fine, but I do have two ideas for mitigations.

  • Pass the callbacks through a proxy that is intended to be untrusted (i.e. no one should ever provide is an ERC20 approval or grant it a privileged role in any way). The market contract itself will only ever call the proxy.
  • Remove the journal from the directly sent data and instead stash it in transient storage. A call from the called contract can get journal's digest or contents. This has a few challenges though, including storing arbitrary length byte strings in transient storage.

Not sure what the right level of caution is here, or the right mitigation if caution is warranted.

Is this not a problem with the image that the callback implementer has chosen to use? If it is possible for the image to generate a journal that leads to tokens being transferred to the wrong person, then the image is the problem?

One thing this did make me think of is, would make sense to pass in the Predicate also used for the journal into the callback? Then even for more general purpose images, the callback can verify the journal meets some requirements? [I guess this would require us making the assessor output the predicate]

Re: the two mitigations, I don't fully see how they help. Even with a proxy, or fetching the journal from storage, ultimately there is a contract that needs to read the journal and decide what to do as a result of it- and if the journal can be manipulated, that contract will do the wrong thing.

My description of the potential issue is not super clear, but the issue I was thinking of would require an coincidental collision in the value of the 4 byte selector, and sender-based authentication, as well as meeting other constraints on how it interprets the received data. It is unlikely that this will ever be the case.

@willpote willpote changed the base branch from main to angelo/bm-459-explore-options-for-delivering-unaggregated-proofs March 3, 2025 15:38
Copy link

github-actions bot commented Mar 3, 2025

🚀 Documentation Preview

Deployment URL: https://boundless-documentation-4146xiet0-risczero.vercel.app

Updated at: 2025-03-05 15:02:08 UTC

@willpote
Copy link
Contributor Author

willpote commented Mar 3, 2025

I merged in and changed the base to be the unaggregated proof PR so that I could use the new types there

Base automatically changed from angelo/bm-459-explore-options-for-delivering-unaggregated-proofs to main March 4, 2025 21:58
Copy link
Member

@nategraf nategraf left a comment

Choose a reason for hiding this comment

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

Looks good to me!

I was staring at the logic in fulfillBatch again wondering if we really need 3 branches there. I think I have a nice simplification in that regard by moving the callback before fulfillment is set and using the fact that if anything fails, the whole call will revert anyway.

#364


/// @inheritdoc IBoundlessMarketCallback
function handleProof(bytes32 imageId, bytes calldata journal, bytes calldata seal) public {
require(msg.sender == BOUNDLESS_MARKET, "Invalid sender");
Copy link
Member

Choose a reason for hiding this comment

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

I think this is fine to include, and possibly beneficial, but its not clear to me that this is case. Let's keep this check for now, and clearly folks can always override this function to remove it if they decide it's unnecessary.

Keep in mind that anyone can specify a callback to any contract, and anyone can create a request on the market. So the amount of constraint this applies to the call is limited, beyond the fact that the receipt is already checked, which is rechecked based on our discussion earlier.

nategraf and others added 4 commits March 5, 2025 09:55
@willpote willpote merged commit 40a65cf into main Mar 5, 2025
16 checks passed
@willpote willpote deleted the willpote/callbacks-2 branch March 5, 2025 17:09
willpote added a commit that referenced this pull request Mar 8, 2025
… property (#372)

This PR is a change I am proposing, but do not have consensus on.
Opening it to discuss.

With #335, we added
support for callbacks on fulfillment and we aimed to have the callback
trigger at most once per request. Since that PR, I realized two issues:

1. As implemented, there is a potential re-entrancy attack on this
property in that the called contract can call one of the `fulfill`
methods and have the callback trigger again for the same request (ID).
This is because we use the `fulfilled` flag to indicate whether the
callback has run, but that flag is not set until after the callback is
complete.
2. Even if a given request ID can only be used to trigger a callback
once, any user can create an arbitrary number of unique requests that
are fulfilled by the same proof. Using a single receipt, they can
fulfill an arbitrary number of requests and trigger an identical
callback for each. This is indistinguishable on the called contract from
multiple calls associated with the same request.

My opinion is that the call-once property does not buy us much. If there
is an action which modifies state, the called application will have its
own notion of what to do when the callback arrives, and ensuring it
behaves reasonably if the same call is repeated is a common requirement
for a wide range of applications.

As an advantage to dropping this property as a goal, benchmarks show it
would save ~800 gas per fulfillment.

---------

Co-authored-by: Pote <[email protected]>
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.

3 participants