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

Add Plugin support #53

Merged
merged 4 commits into from
Feb 17, 2023
Merged

Add Plugin support #53

merged 4 commits into from
Feb 17, 2023

Conversation

cleanunicorn
Copy link
Contributor

@cleanunicorn cleanunicorn commented Dec 3, 2022

Implements #42.

Copy link
Owner

@PaulRBerg PaulRBerg left a comment

Choose a reason for hiding this comment

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

Thanks very much @cleanunicorn, this is looking great so far. I left my feedback below.

Additionally, some notes:

  • I didn't finish the migration to Foundry in this repo, so wherever you feel like a component is missing, please do take a look at prb-math and prb-contracts for inspiration, because both are already shipped.
  • It looks like you have committed the artifacts in version control - perhaps this was a left-over from a previous checkout in which you had compiled the PRBProxy contracts with Hardhat. The Hardhat artifacts should be deleted.
  • The tests are not structured correctly.
    • There is no need for a base PluginTest because the plugin functionality is incorporated in PRBProxy, and so the plugin-related events and variables should be added in the existing PRBProxyTest contract.
    • Crucially, this repo follows the testing trees/ branching approach. A fellow Ethereum developer called Sabnock who likes this testing approach wrote an article about it (he calls it "state building").
    • The easiest way to understand what I mean by this would be to take a look at the PRBMath test trees and the PRBContract test trees
    • To ensure a smooth developer experience while writing the tests trees, I recommend installing the vscode-tree-language VSCode extension to get syntax highlighting for the *.tree files.
    • The tests are meant to be separated with empty modifiers.
  • While the Angular Commit Message Format doesn't have many hard-and-fast rules, I would have used different commit types than the ones that you have used so far:
    • Implementing the plugin is a change of type feat instead of chore.
    • Adding events in a smart contract is a change of type feat instead of chore (because you add functionality); also, when bundling lots of charge under the roof of one commit, it's helpful to add more information in the git commit body, e.g. in commit 306d0ef an additional docs: document plugin functions with NatSpec would have been helpful.
    • Adding tests is a change of type test instead of chore.
    • Fixing linting/ styling errors is a change of type chore or style instead of fix (the latter is reserved for smart contract bug fixes).

And some questions:

  1. I'm just thinking out loud, but doesn't this plugin functionality introduce a vulnerability whereby a malicious target contract can call any arbitrary plugin by calling the fallback function on the proxy itself, after the user has called the target contract via the normal execute pathway?
  2. May I have your permission to edit the git commit messages based on the feedback above? Or, alternatively, you could do it. The idea would be to git push with --force.

src/IPRBProxy.sol Outdated Show resolved Hide resolved
src/IPRBProxy.sol Outdated Show resolved Hide resolved
src/IPRBProxy.sol Outdated Show resolved Hide resolved
src/IPRBProxy.sol Outdated Show resolved Hide resolved
src/IPRBProxy.sol Outdated Show resolved Hide resolved
src/PRBProxy.sol Outdated Show resolved Hide resolved
src/PRBProxy.sol Outdated Show resolved Hide resolved
src/IPRBProxy.sol Outdated Show resolved Hide resolved
src/PRBProxy.sol Outdated Show resolved Hide resolved
src/PRBProxy.sol Outdated Show resolved Hide resolved
@cleanunicorn
Copy link
Contributor Author

cleanunicorn commented Dec 5, 2022

And some questions:

  1. I'm just thinking out loud, but this plugin functionality introduce a vulnerability whereby a malicious target contract can call any arbitrary plugin by calling the fallback function on the proxy itself, after the user has called the target contract via the normal execute pathway?

We will need to think deeply about security issues that might appear since this does a delegatecall. We should properly think about attack vectors once the plugin system is closer to finality.

Currently there is no permission schema for plugins.

I don't understand the example you're explaining. I'm not sure this is an issue since the executed code should still be trusted (is this right?). Could you add more details?

Also, it's important to understand trusted and untrusted actors in the system.

  1. May I have your permission to edit the git commit messages based on the feedback above? Or, alternatively, you could do it. The idea would be to git push with --force.

I tried rewriting, but feel free to push commits (rewrite history), I gave you permission.

@newts6
Copy link

newts6 commented Dec 5, 2022

Excuse my interjection here :)

Re:

  1. I'm just thinking out loud, but this plugin functionality introduce a vulnerability whereby a malicious target contract can call any arbitrary plugin by calling the fallback function on the proxy itself, after the user has called the target contract via the normal execute pathway?

It might be worth having a plugin (or method) type which gets only invoked with a staticcall instead of delegatecall.

@cleanunicorn
Copy link
Contributor Author

cleanunicorn commented Dec 6, 2022

Open questions:

  1. I still need additional information regarding the security issues. At the moment, I don't see an attack vector.
  2. @newts6, how would the static call be useful? Can you give an example?
  3. Installing a plugin can possibly overwrite another plugin. Should we make sure the installedPluginMethods[methodList[i]] == 0? Could we add an argument to install(plugin, bool overwrite)?
  4. Uninstalling a plugin can potentially disable a method from another plugin. Should I make sure the removed method is from the specified plugin before disabling it?

@newts6
Copy link

newts6 commented Dec 6, 2022

2. @newts6, how would the static call be useful? Can you give an example?

Assurance of a reduced attack surface with non state changing calls would allow a more modular security policy for plugin installers. i.e. being way more relaxed with read only plugins.

Several use cases could benefit, first and foremost EIP1271 implementations ofc. But also I suppose most callbacks for ERC721/1155 safe transfers.

@cleanunicorn cleanunicorn changed the title [WIP] Add Plugin support Add Plugin support Dec 19, 2022
@PaulRBerg
Copy link
Owner

PaulRBerg commented Jan 8, 2023

Replies

I'm not sure this is an issue since the executed code should still be trusted (is this right?). Could you add more details?

I think you're right - if you give permission to a target contract and you delegate-call to it, then if they are malicious they can do other things directly. They don't need to jump through hoops and use the plugin system.

Target contracts are indeed trusted actors under the current design of PRBProxy, and the plugin system you have implemented is orthogonal to it.

feel free to push commits (rewrite history), I gave you permission

Thank you! I pushed with --force to update the historical commit messages.

NOTE: you might have to delete your local feat/plugins to be able to fetch the latest changes.

  1. Installing a plugin can possibly overwrite another plugin

Good catch, but I think that this is fine. Plugins should normally not overlap, just like ERCs don't usually overlap.

The complexity of implementing an overwrite check, and the maintenance cost do not seem worth it to me. Similarly, with the bool overwrite proposal - the overhead is not worth it.

Also, this is a rectifiable scenario - in case a user accidentally overwrites a plugin, they can simply uninstall it, and then install a new one that doesn't have the overwritten methods.

  1. Should I make sure the removed method is from the specified plugin before disabling it?

In a similar vein to the above, I would classify this as a rectifiable issue that can easily be patched manually by the user.

I would have been open to implementing these checks if we had a different access control schema that allowed third-parties to call the installPlugin and uninstallPlugin functions on behalf of the user - in that case, a user could easily be caught off-guard by an accidental overwrite triggered by someone else.

But, since in our implementation, it is the user only who can install and uninstall plugins, they should be well aware of what they are installing.

Also worth mentioning is that the "user" referred to above wouldn't actually be the end user, i.e. the owner of the proxy - but rather a frontend developer who would parse the proxy API on behalf of the actual end user.

Feedback

On top of what I left in the comments above.

1. Test Function Names

This is something that I learned in the meantime - a few Foundry contributors are working on a new Foundry-based linter for Solidity called scopelint, which will use a specific regex for linting test function names. See point 2 in General Test Guidance section of the Foundry Book for more detail; for now, what matters is that I have updated your test names to adhere to these best practices. I will also update the base branch staging to follow this format, and then I will do the rebase myself.

2. Name of execute directory

I think that it is misleading to use the same directory name for the plugin execution tests. I have renamed it to run-plugin to make a clear distinction between the core execution function tests, and the plugin runs.

3. No testing trees

The tests still do not conform with the testing tree approach - there are no *.tree files.

But don't worry, I know I have asked for a lot already. Thank you for getting the ball rolling with the first suite of tests - I will refactor them to adhere to the system I have in place.

4. Install and Uninstall Tests should be separated

Different functions are always tested separately.

src/IPRBProxy.sol Outdated Show resolved Hide resolved
src/IPRBProxy.sol Outdated Show resolved Hide resolved
src/PRBProxy.sol Outdated Show resolved Hide resolved
src/PRBProxy.sol Outdated Show resolved Hide resolved
src/PRBProxy.sol Outdated Show resolved Hide resolved
src/PRBProxy.sol Outdated Show resolved Hide resolved
test-sol/prb-proxy/PRBProxyTest.t.sol Outdated Show resolved Hide resolved
test-sol/prb-proxy/PRBProxyTest.t.sol Outdated Show resolved Hide resolved
test-sol/shared/TargetPluginRevert.t.sol Outdated Show resolved Hide resolved
test-sol/prb-proxy/plugins/install/install.t.sol Outdated Show resolved Hide resolved
@PaulRBerg
Copy link
Owner

PaulRBerg commented Jan 8, 2023

Also, @cleanunicorn, FYI - I have pushed a big commit to address all of the feedback left in the code review comments.

I am still due to refactor the tests to adhere to the testing tree approach - but once that's done, we can merge this in, after you review my feedback, too, of course.

Update: I have managed to complete the migration to Foundry on the staging branch, which has introduced a few git conflicts with this PR. I will fix them myself.

@PaulRBerg PaulRBerg force-pushed the feat/plugins branch 2 times, most recently from c17b588 to 682ae1f Compare January 8, 2023 15:57
@PaulRBerg PaulRBerg force-pushed the staging branch 2 times, most recently from b308058 to 15d23fd Compare February 11, 2023 15:03
Repository owner deleted a comment from codecov-commenter Feb 12, 2023
src/PRBProxy.sol Outdated Show resolved Hide resolved
@PaulRBerg
Copy link
Owner

PaulRBerg commented Feb 13, 2023

After pushing a few commits these days, we are now almost ready to merge this PR.

@cleanunicorn, could you take a final look at the latest changes to confirm that you're happy with the aesthetic changes I made (e.g. rename IPlugin to IPRBProxyPlugin)?

Repository owner deleted a comment from codecov-commenter Feb 13, 2023
@PaulRBerg PaulRBerg marked this pull request as ready for review February 13, 2023 14:50
@PaulRBerg
Copy link
Owner

Two final security-related points from my end.

Strange Loops

Is it possible to install the proxy itself as a plugin by running the following steps:

  1. Create a (malicious?) plugin contract that returns the methodList method selector itself in its implementation of methodList, among other methods implemented in PRBProxy itself (e.g. transferOwnership).
  2. Call installPlugin and pass the proxy's address. The proxy itself will get stored as the associated plugin for the methodList method and the other method selectors, because the call to methodList will trigger a call to the fallback function, which would call the malicious plugin from step 1, which would return its method list.

Now, the fun part: calling any method not defined in the proxy itself will lead to infinite recursion in the fallback function, triggering a stack overflow.

However, calling a method defined in the proxy would result in a successful transaction. The question is whether this can lead to any exploit.

I don't think it can because all essential functions are protected by owner != msg.sender checks. Calling these functions via the fallback function would mean jumping through hoops to achieve the same goal.

Therefore, adding an explicit check in the installPlugin function that would prevent the proxy itself from becoming a plugin doesn't seem helpful to me - the gas cost would increase, but no security value would be added.

Empty Data

When the calldata is empty, the receive function gets called.

@PaulRBerg
Copy link
Owner

The PR LGTM now. Test coverage is 100%:

Screenshot 2023-02-13 at 8 20 34 PM

@PaulRBerg
Copy link
Owner

There are conflicts again .. I will fix them tomorrow.

feat: implements Plugin Install and Uninstall

feat: add events when installing uninstalling plugins

feat: add plugin execute tests

fix: handle lint errors

chore: remove artifacts folder

refactor: change error names to match overall style

refactor: reorder imports and methods to be alphabetically sorted

refactor: reorder storage to be alphabetically

refactor: move plugin tests and include plugin test setup in PRBProxyTest

fix: use correct errors for plugin

chore: remove unnecessary solhint ignore

refactor: optimize loops by removing overflow checks

chore: mark `methodList` as `pure` to silence linting warning

test: check only owner can uninstall plugins

test: split plugin install tests with modifiers

fix: installing should ensure at least one method is installed

docs: document plugin interface with NatSpec and update license to MIT

test: reoder imports in execute test

test: remove commented out code in TargetPluginRevert

refactor: use "IPlugin" type for plugin addresses

chore: fix formatting in ".gitmodules"
docs: improve wording in NatSpec comments
perf: cache array length
refactor: add check for empty method list in "uninstallPlugin"
refactor: add "plugin" param to "PRBProxy__PluginExecutionReverted" error
refactor: organize functions in "PRBProxy"
refactor: rename "getPluginForSignature" to "getPluginForMethod"
refactor: rename "PRBProxy__PluginMethodNotFound" to "PRBProxy__NoPluginMethods"
test: add "Plugins" struct
test: add assertion to compare two "IPlugin" addresses
test: move plugins to the "Plugins" struct entity
test: rename "TargetPluginRevert" to just "PluginRevert"
test: refactor plugin test names to conform to Foundry best practices
test: separate "installPlugin" from "uninstallPlugin" tests

refactor: fix rebase conflicts and issues

docs: improve wording in NatSpec comments

chore: improve wording in code comments
docs: add "Notes" section in NatSpec comments of plugin functions
test: add modifiers in "installPlugin" and "uninstallPlugin" tests
test: add state trees for "installPlugin" and "uninstallPlugin"
test: add test when plugin has not methods in "uninstallPlugin" tests
test: add test when plugin installed in "installPlugin" tests
test: add test when plugin not installed in "uninstallPlugin" tests
test: explicitly define "actualPlugin" and "expectedPlugini"
test: improve wording in comments and functions
test: use "eve" instead of "bob" for "caller not owner" tests

refactor: rename "IPlugin" to "IPRBProxyPlugin"

feat: add "RunPlugin" event in fallback function

chore: ignore "no-complex-fallback" in Solhint config
chore: nest "ignored_error_codes" on multiple lines in Foundry config
docs: enhance NatSpec code comments
refactor: dry-ify delegate call in internal "_safeDelegateCall" function
refactor: rename "PluginExecutionReverted" to "PluginReverted"
refactor: rename "NoPluginFound" to "PluginNotInstalledForMethod"
test: add "<0.9.0" pragma bound in all test files
test: add more plugin helpers
test: fix variable names in eth transfer tests
test: fix typos in test descriptions
test: fully test plugin runs
test: improve wording in tests
test: inherit from targets in plugins
test: remove arithmetic underflow tests
test: rename "revert" to "reverter"
test: rename "Struct" to "SomeStruct"
test: rename "TargetError" to "SomeError"
test: rename "TargetRevert" to "TargetReverter"
test: rename "TargetSelfDestruct" to "TargetSelfDestructer"
test: test index OOB in "execute" function
@PaulRBerg
Copy link
Owner

There were many conflicts and frankly I was unsure what was the fix for several of them - to avoid the risk of deleting something accidentally, I have squashed all commits in one commit (was going to do this anyway, before merging), and rebased.

PR ready for your final review, @cleanunicorn!

@cleanunicorn
Copy link
Contributor Author

Strange Loops

Is it possible to install the proxy itself as a plugin by running the following steps:

  1. Create a (malicious?) plugin contract that returns the methodList method selector itself in its implementation of methodList, among other methods implemented in PRBProxy itself (e.g. transferOwnership).

It is possible for the plugin to try to overwrite transferOwnership, however the PRBProxy.transferOwnership will take precedence over the method defined in the plugin. If no method in the PRBProxy matches the one called, then the fallback method is activated. Thus, the proxy can't overwrite methods in PRBProxy.

  1. Call installPlugin and pass the proxy's address. The proxy itself will get stored as the associated plugin for the methodList method and the other method selectors, because the call to methodList will trigger a call to the fallback function, which would call the malicious plugin from step 1, which would return its method list.

Now, the fun part: calling any method not defined in the proxy itself will lead to infinite recursion in the fallback function, triggering a stack overflow.

However, calling a method defined in the proxy would result in a successful transaction. The question is whether this can lead to any exploit.

I don't think it can because all essential functions are protected by owner != msg.sender checks. Calling these functions via the fallback function would mean jumping through hoops to achieve the same goal.

Therefore, adding an explicit check in the installPlugin function that would prevent the proxy itself from becoming a plugin doesn't seem helpful to me - the gas cost would increase, but no security value would be added.

Any method defined in PRBProxy is tried before any method in an installed proxy is called. This should protect from multiple attack vectors since it uses the security properties of PRBProxy.

Empty Data

When the calldata is empty, the receive function gets called.

This should be regarded as receiving ether. I believe we desire this behavior.

foundry.toml Outdated Show resolved Hide resolved
@PaulRBerg
Copy link
Owner

Thanks for the final review, @cleanunicorn! I agree with all the points you made.

@PaulRBerg PaulRBerg merged commit 7daea25 into PaulRBerg:staging Feb 17, 2023
@PaulRBerg PaulRBerg deleted the feat/plugins branch February 17, 2023 09:35
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