Skip to content

Conversation

@PaulRBerg
Copy link
Owner

@PaulRBerg PaulRBerg commented Jun 29, 2023

Addresses https://github.com/cantinasec/review-sablier2/issues/1.

Summary by OpenAI

Release Notes:

  • New Feature: The PRBProxyRegistry contract now checks for method collisions before installing a plugin, reverting with information about conflicting plugins and methods if a collision is detected.
  • Documentation: The comment in the IPRBProxy.sol interface has been modified to clarify that calls are delegated to the target contract by forwarding the data.
  • Documentation: The IPRBProxyRegistry.sol interface introduces a new error type for plugin method collisions and updates the documentation for the installPlugin function accordingly.
  • Test: The Base.t.sol test file adds an import statement and creates an instance of PluginCollider in the Plugins struct.
  • Test: Two new plugin contracts, PluginCollider and PluginSablier, have been introduced, implementing the IPRBProxyPlugin interface and defining relevant methods.
  • Test: The install-plugin and uninstall-plugin test files have been modified to refactor test functions, add new tests for method collisions, and update test case descriptions.

"Code changes dance,
Bugs have no chance.
Features shine bright,
Tests bring delight.
With every commit,
We make code fit."

test: adjust tests in light of new contract logic
test: improve writing in state trees
@PaulRBerg PaulRBerg added the bug label Jun 29, 2023
@PaulRBerg PaulRBerg merged commit 4f89559 into main Jun 29, 2023
@PaulRBerg PaulRBerg deleted the fix/plugin-method-collision branch June 29, 2023 13:37
@github-actions
Copy link

🤖 OpenAI

High-level summary:

The changes include improvements to the PRBProxyRegistry contract and related interfaces, as well as updates to test cases. The PRBProxyRegistry contract now checks for method collisions before installing a plugin and reverts if a collision is detected. The IPRBProxy.sol interface has a minor comment modification. The IPRBProxyRegistry.sol interface introduces a new error type and updates the documentation for the installPlugin function. Test cases have been refactored and expanded to cover method collisions and plugin installation scenarios.

Table of file summaries:

File Summary
src/PRBProxyRegistry.sol Checks for method collisions before installing a plugin and reverts with information about conflicting plugins and methods.
src/interfaces/IPRBProxy.sol Modifies a comment in the code to clarify that calls are delegated to the target contract by forwarding the data.
src/interfaces/IPRBProxyRegistry.sol Introduces a new error type for method collisions and updates the documentation for the installPlugin function accordingly.
test/Base.t.sol Adds an import statement for PluginCollider.sol and creates an instance of PluginCollider in the Plugins struct.
test/mocks/plugins/PluginCollider.sol Introduces a new contract PluginCollider that implements the IPRBProxyPlugin interface. Adds a method methodList() and implements the onAddictionFeesRefunded() function, which may cause collisions.
test/mocks/plugins/PluginSablier.sol Introduces a new contract PluginSablier that implements the IPRBProxyPlugin interface. Defines a method methodList() and an onStreamCanceled() function.
test/registry/install-plugin/installPlugin.t.sol Refactors test functions for installing a plugin, replaces duplicate code with a single installation, and adds a new test function to check for method collisions.
test/registry/install-plugin/installPlugin.tree Modifies the test case for installing a plugin, introducing scenarios with and without method collisions.
test/registry/uninstall-plugin/uninstallPlugin.t.sol Updates test cases for uninstalling a plugin, changing the modifier name and updating a comment.
test/registry/uninstall-plugin/uninstallPlugin.tree Modifies the wording in a test case description to clarify that the plugin has been installed before.

Chat with 🤖 OpenAI Bot (@openai)

  • Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
  • Invite the bot into a review comment chain by tagging @openai in a reply.

Code suggestions

  • The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
  • You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.

Ignoring further reviews

  • Type @openai: ignore anywhere in the PR description to ignore further reviews from the bot.

Files not summarized due to errors (1)

Failed to summarize

  • test/utils/Precompiles.sol (diff tokens exceeds limit)

In the recent run, only the files that changed from the base of the PR and between a4682f84008854e45cf4b640acdeea8ca2598598 and 927736b0c87f316169d495a658fa8e192efc8e3c commits were reviewed.

Comment on lines 18 to +29
/// @notice Thrown when an action requires the owner to not have a proxy.
error PRBProxyRegistry_OwnerHasProxy(address owner, IPRBProxy proxy);

/// @notice Thrown when installing or uninstall a plugin, and the plugin doesn't implement any method.
/// @notice Thrown when trying to install or uninstall a plugin, and the plugin doesn't implement any method.
error PRBProxyRegistry_PluginEmptyMethodList(IPRBProxyPlugin plugin);

/// @notice Thrown when trying to install a plugin that implements a method that is already implemented by another
/// installed plugin.
error PRBProxyRegistry_PluginMethodCollision(
IPRBProxyPlugin currentPlugin, IPRBProxyPlugin newPlugin, bytes4 method
);

Choose a reason for hiding this comment

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

🤖 OpenAI

The error messages have been updated to provide more specific information about the errors. No issues found.

Comment on lines 141 to 153
function deploy() external returns (IPRBProxy proxy);

/// @notice Deploys a new proxy via CREATE2 by setting the caller as the owner, and delegate calls to the provided
/// target contract by forwarding the data. It returns the data it gets back, bubbling up any potential revert.
/// target contract by forwarding the data. It returns the data it gets back, and bubbles up any potential revert.
///
/// @dev Emits a {DeployProxy} and an {Execute} event.
///
/// Requirements:
/// - The caller must not have a proxy.
/// - All from {PRBProxy.execute}.
/// - `target` must be a contract.
///
/// @param target The address of the target contract.
/// @param data Function selector plus ABI encoded data.

Choose a reason for hiding this comment

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

🤖 OpenAI

The deploy function has been modified to include additional documentation and requirements. No issues found.

Comment on lines 10 to 22
import { PRBProxy } from "../src/PRBProxy.sol";
import { PRBProxyRegistry } from "../src/PRBProxyRegistry.sol";

import { PluginCollider } from "./mocks/plugins/PluginCollider.sol";
import { PluginDummy } from "./mocks/plugins/PluginDummy.sol";
import { PluginEcho } from "./mocks/plugins/PluginEcho.sol";
import { PluginEmpty } from "./mocks/plugins/PluginEmpty.sol";
import { PluginPanic } from "./mocks/plugins/PluginPanic.sol";
import { PluginReverter } from "./mocks/plugins/PluginReverter.sol";
import { PluginSablier } from "./mocks/plugins/PluginSablier.sol";
import { PluginSelfDestructer } from "./mocks/plugins/PluginSelfDestructer.sol";
import { TargetDummy } from "./mocks/targets/TargetDummy.sol";
import { TargetDummyWithFallback } from "./mocks/targets/TargetDummyWithFallback.sol";

Choose a reason for hiding this comment

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

🤖 OpenAI

The import statements have been modified to include additional plugin files. No issues found.

Comment on lines 34 to 45
//////////////////////////////////////////////////////////////////////////*/

struct Plugins {
PluginCollider collider;
PluginDummy dummy;
PluginEcho echo;
PluginEmpty empty;
PluginPanic panic;
PluginReverter reverter;
PluginSablier sablier;
PluginSelfDestructer selfDestructer;
}

Choose a reason for hiding this comment

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

🤖 OpenAI

A new struct Plugins has been introduced, which includes instances of various plugins. No issues found.

Comment on lines +59 to 66
/// @notice Delegate calls to the provided target contract by forwarding the data. It returns the data it
/// gets back, and bubbles up any potential revert.
///
/// @dev Emits an {Execute} event.
///
/// Requirements:
/// - The caller must be either an owner or an envoy with permission.
/// - The caller must be either the owner or an envoy with permission.
/// - `target` must be a contract.

Choose a reason for hiding this comment

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

🤖 OpenAI

The comment has been modified to clarify that calls are delegated to the target contract by forwarding the data. However, there is a typo in the comment ("It returns the data it gets back"). It should be "It returns the data it receives back". Please update the comment accordingly.

Suggested change
/// @notice Delegate calls to the provided target contract by forwarding the data. It returns the data it
/// gets back, and bubbles up any potential revert.
///
/// @dev Emits an {Execute} event.
///
/// Requirements:
/// - The caller must be either an owner or an envoy with permission.
/// - The caller must be either the owner or an envoy with permission.
/// - `target` must be a contract.
/// @notice Delegate calls to the provided target contract by forwarding the data. It returns the data it receives back, and bubbles up any potential revert.
///
/// @dev Emits an {Execute} event.
///
/// Requirements:
/// - The caller must be either the owner or an envoy with permission.
/// - `target` must be a contract.
///
/// @param target The address of the target contract.

Comment on lines 5 to 7
├── when the plugin list is empty
│ └── it should revert
└── when the plugin list is not empty

Choose a reason for hiding this comment

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

🤖 OpenAI

The test case structure has changed. Please update the description of the test cases accordingly to reflect the new structure.

Comment on lines +8 to +9
├── when there is a method collision
│ └── it should revert

Choose a reason for hiding this comment

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

🤖 OpenAI

The test case structure has changed. Please update the description of the test cases accordingly to reflect the new structure.

Comment on lines 11 to 12
├── it should install the plugin
└── it should emit an {InstallPlugin} event

Choose a reason for hiding this comment

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

🤖 OpenAI

The test case structure has changed. Please update the description of the test cases accordingly to reflect the new structure.

Comment on lines 193 to 208
// Install every method in the list.
address owner = msg.sender;
for (uint256 i = 0; i < length;) {
_plugins[owner][methodList[i]] = plugin;
// Check for collisions.
bytes4 method = methodList[i];
if (address(_plugins[owner][method]) != address(0)) {
revert PRBProxyRegistry_PluginMethodCollision({
currentPlugin: _plugins[owner][method],
newPlugin: plugin,
method: method
});
}
_plugins[owner][method] = plugin;
unchecked {
i += 1;
}

Choose a reason for hiding this comment

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

🤖 OpenAI

The code introduces a method collision check before installing a plugin in the PRBProxyRegistry contract. This is a good addition to prevent conflicts between plugins. However, there are a few improvements that can be made:

  1. Line 198: Instead of comparing the address of _plugins[owner][method] with address(0), you can use the == operator to directly check if it's equal to zero. This simplifies the condition.

  2. Line 199: The revert statement should include a reason string to provide more information about the error. You can use the string type for the reason and pass it as an argument to the revert statement.

  3. Lines 200-203: The PRBProxyRegistry_PluginMethodCollision event is being reverted with the conflicting plugin and method information. It would be helpful to define this event in the contract and emit it before reverting. This allows external parties to listen to the event and capture the details of the collision.

  4. Line 207: Instead of using unchecked, you can increment i using the regular i++ syntax.

Here's the updated code with the suggested improvements:

// Define the event
event PRBProxyRegistry_PluginMethodCollision(address currentPlugin, address newPlugin, bytes4 method);

// Install every method in the list.
address owner = msg.sender;
for (uint256 i = 0; i < length;) {
    // Check for collisions.
    bytes4 method = methodList[i];
    if (_plugins[owner][method] != address(0)) {
        emit PRBProxyRegistry_PluginMethodCollision(_plugins[owner][method], plugin, method);
        revert("PRBProxyRegistry: Plugin method collision");
    }
    _plugins[owner][method] = plugin;
    i++;
}

Note: Make sure to define the PRBProxyRegistry_PluginMethodCollision event in the contract.

Comment on lines 7 to 8
└── when the plugin list is not empty
├── when the plugin has not been installed before

Choose a reason for hiding this comment

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

🤖 OpenAI

The comment "when the plugin has not been installed before" is incorrect. It should be "when the plugin has not been installed before and there are no method collisions".

Comment on lines +1 to +2
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.18;

Choose a reason for hiding this comment

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

🤖 OpenAI

The SPDX license identifier is missing. Please add the appropriate SPDX license identifier to the contract.

Suggested change
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.18;
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.18;

6:
The contract PluginCollider implements the IPRBProxyPlugin interface, but it doesn't define the required function name. Please add the name function to the contract.

Suggested change
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.18;
function name() external pure override returns (string memory) {
return "PluginCollider";
}

Comment on lines +7 to +10
function methodList() external pure override returns (bytes4[] memory) {
bytes4[] memory methods = new bytes4[](1);
methods[0] = this.onAddictionFeesRefunded.selector;
return methods;

Choose a reason for hiding this comment

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

🤖 OpenAI

The methodList function returns an array of bytes4 values representing the selectors of the methods provided by the plugin. Currently, it only returns a single selector (this.onAddictionFeesRefunded.selector). If you intend to have multiple methods in this plugin, you should update the methods array accordingly. Otherwise, if there's only one method, you can simplify the code by directly returning the selector without using an array.

Suggested change
function methodList() external pure override returns (bytes4[] memory) {
bytes4[] memory methods = new bytes4[](1);
methods[0] = this.onAddictionFeesRefunded.selector;
return methods;
function methodList() external pure override returns (bytes4[] memory) {
bytes4[] memory methods = new bytes4[](1);
methods[0] = this.onAddictionFeesRefunded.selector;
return methods;
}

Comment on lines +13 to +18
/// @dev The selector for this method is 0x72eba203, which is the same as the selector for
/// `onStreamCanceled(uint256,address,uint128,uint128)`
function onAddictionFeesRefunded(uint248 loanId, int168, uint192 feeAmount, int248) external pure {
loanId;
feeAmount;
}

Choose a reason for hiding this comment

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

🤖 OpenAI

The onAddictionFeesRefunded function has unused parameters loanId and feeAmount. If these parameters are not needed, you can remove them from the function signature to avoid confusion.

Suggested change
/// @dev The selector for this method is 0x72eba203, which is the same as the selector for
/// `onStreamCanceled(uint256,address,uint128,uint128)`
function onAddictionFeesRefunded(uint248 loanId, int168, uint192 feeAmount, int248) external pure {
loanId;
feeAmount;
}
function onAddictionFeesRefunded(uint248, int168, uint192, int248) external pure {
// No need to use the parameters
}

Overall, the changes made to test/mocks/plugins/PluginCollider.sol look good with the suggested modifications.

Comment on lines 35 to 41
// Uninstall the plugin.
registry.uninstallPlugin(plugins.dummy);

// Assert that every plugin method has been uninstalled.
// Assert that every plugin method has remained uninstalled.
bytes4[] memory pluginMethods = plugins.dummy.methodList();
for (uint256 i = 0; i < pluginMethods.length; ++i) {
IPRBProxyPlugin actualPlugin = registry.getPluginByOwner({ owner: users.alice, method: pluginMethods[i] });

Choose a reason for hiding this comment

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

🤖 OpenAI

The code is checking that every plugin method has remained uninstalled after uninstalling the plugin. However, the current implementation retrieves the plugin methods using plugins.dummy.methodList(), which assumes that the plugin is still installed. This can lead to incorrect results if the plugin has already been uninstalled. To fix this, you should retrieve the plugin methods from the registry instead of directly from the plugin. Here's the updated code:

// Assert that every plugin method has remained uninstalled.
bytes4[] memory pluginMethods = registry.getPluginMethods(plugins.dummy);
for (uint256 i = 0; i < pluginMethods.length; ++i) {
    IPRBProxyPlugin actualPlugin = registry.getPluginByOwner({ owner: users.alice, method: pluginMethods[i] });
    // Rest of the code...
}

Comment on lines 44 to 51
}
}

modifier whenPluginInstalled() {
modifier whenPluginInstalledBefore() {
// Install the dummy plugin.
registry.installPlugin(plugins.dummy);
_;
}

Choose a reason for hiding this comment

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

🤖 OpenAI

The modifier whenPluginInstalledBefore is used to install the dummy plugin before executing the test case. However, the current implementation installs the plugin using registry.installPlugin(plugins.dummy) but doesn't uninstall it afterward. This means that subsequent test cases may have unexpected behavior due to the plugin being installed. To ensure a clean state for each test case, you should uninstall the plugin at the end of the modifier. Here's the updated code:

modifier whenPluginInstalledBefore() {
    // Install the dummy plugin.
    registry.installPlugin(plugins.dummy);
    _;
    // Uninstall the plugin.
    registry.uninstallPlugin(plugins.dummy);
}

Comment on lines 63 to 69
}
}

function test_UninstallPlugin_Event() external whenCallerHasProxy whenPluginHasMethods whenPluginInstalled {
function test_UninstallPlugin_Event() external whenCallerHasProxy whenPluginHasMethods whenPluginInstalledBefore {
vm.expectEmit({ emitter: address(registry) });
emit UninstallPlugin({ owner: users.alice, proxy: proxy, plugin: plugins.dummy });
registry.uninstallPlugin(plugins.dummy);

Choose a reason for hiding this comment

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

🤖 OpenAI

The test case is emitting an UninstallPlugin event before calling registry.uninstallPlugin(plugins.dummy). However, the event emission is not necessary since the uninstallPlugin function itself emits the event. You can remove the unnecessary event emission. Here's the updated code:

function test_UninstallPlugin_Event() external whenCallerHasProxy whenPluginHasMethods whenPluginInstalledBefore {
    vm.expectEmit({ emitter: address(registry) });
    registry.uninstallPlugin(plugins.dummy);
    // Rest of the code...
}

Comment on lines +34 to +43
function test_RevertWhen_MethodCollision() external whenCallerHasProxy whenPluginListNotEmpty {
registry.installPlugin(plugins.sablier);
vm.expectRevert(
abi.encodeWithSelector(
IPRBProxyRegistry.PRBProxyRegistry_PluginMethodCollision.selector,
plugins.sablier,
plugins.collider,
plugins.sablier.onStreamCanceled.selector
)
);

Choose a reason for hiding this comment

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

🤖 OpenAI

The new code introduces a test case test_RevertWhen_MethodCollision that checks for method collisions when installing a plugin. It encodes the necessary parameters and expects a revert if a collision is detected. However, the implementation of this test case seems incorrect. The current implementation installs the sablier plugin, then attempts to check for a collision between sablier.onStreamCanceled and collider. However, it should be checking for a collision between sablier.onStreamCanceled and sablier. Additionally, the test case does not handle the scenario where there is no collision. Consider updating the test case to cover both scenarios: one with a collision and one without a collision.

Comment on lines +51 to 54
function test_InstallPlugin() external whenCallerHasProxy whenPluginListNotEmpty whenNoMethodCollision {
// Install a dummy plugin that has some methods.
registry.installPlugin(plugins.dummy);

Choose a reason for hiding this comment

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

🤖 OpenAI

The new code introduces a test case test_InstallPlugin that installs a dummy plugin and asserts that all its methods have been installed. However, the implementation of this test case seems incorrect. The current implementation installs the dummy plugin but does not perform any assertions or verifications. Consider adding assertions to verify that the plugin methods have been installed correctly.

Comment on lines 61 to 67
}
}

function test_InstallPlugin_Event() external whenCallerHasProxy whenPluginListNotEmpty whenPluginNotInstalled {
function test_InstallPlugin_Event() external whenCallerHasProxy whenPluginListNotEmpty whenNoMethodCollision {
vm.expectEmit({ emitter: address(registry) });
emit InstallPlugin({ owner: users.alice, proxy: proxy, plugin: plugins.dummy });
registry.installPlugin(plugins.dummy);

Choose a reason for hiding this comment

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

🤖 OpenAI

The new code introduces a test case test_InstallPlugin_Event that emits an InstallPlugin event and installs a dummy plugin. However, the implementation of this test case seems incorrect. The current implementation emits the event before installing the plugin, which may lead to confusion. Consider moving the emission of the event after installing the plugin to ensure the event accurately reflects the state change.

function test_RevertWhen_MethodCollision() external whenCallerHasProxy whenPluginListNotEmpty {
    registry.installPlugin(plugins.sablier);
    // Check for collision between sablier.onStreamCanceled and sablier.
    vm.expectRevert(
        abi.encodeWithSelector(
            IPRBProxyRegistry.PRBProxyRegistry_PluginMethodCollision.selector,
            plugins.sablier,
            plugins.sablier,
            plugins.sablier.onStreamCanceled.selector
        )
    );
    // Check for collision between sablier.onStreamCanceled and collider.
    vm.expectRevert(
        abi.encodeWithSelector(
            IPRBProxyRegistry.PRBProxyRegistry_PluginMethodCollision.selector,
            plugins.sablier,
            plugins.collider,
            plugins.sablier.onStreamCanceled.selector
        )
    );
    registry.installPlugin(plugins.collider);
}

function test_InstallPlugin() external whenCallerHasProxy whenPluginListNotEmpty whenNoMethodCollision {
    // Install a dummy plugin that has some methods.
    registry.installPlugin(plugins.dummy);

    // Assert that every plugin method has been installed.
    bytes4[] memory pluginMethods = plugins.dummy.methodList();
    for (uint256 i = 0; i < pluginMethods.length; ++i) {
        IPRBProxyPlugin actualPlugin = registry.getPluginByOwner({ owner: users.alice, method: pluginMethods[i] });
        IPRBProxyPlugin expectedPlugin = plugins.dummy;
        assertEq(actualPlugin, expectedPlugin, "plugin method not installed");
    }
}

function test_InstallPlugin_Event() external whenCallerHasProxy whenPluginListNotEmpty whenNoMethodCollision {
    registry.installPlugin(plugins.dummy);
    vm.expectEmit({ emitter: address(registry) });
    emit InstallPlugin({ owner: users.alice, proxy: proxy, plugin: plugins.dummy });
}

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.

2 participants