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

OZ-L10, SB-L53, SB-L54, SB-M78; Check code length on Notifications, Unsubscribe Minimum Gas Limit #318

Merged
merged 19 commits into from
Sep 4, 2024

Conversation

saucepoint
Copy link
Collaborator

@saucepoint saucepoint commented Aug 30, 2024

Related Issue

Which issue does this pull request resolve?

OZ-L10: calls to addresses without bytecode will still hard-revert, even with try/catch

For chains that still support selfdestruct, you can subscribe to a valid contract but then lose access to unsubscribing if the contract self-destructs


SB L53: Notifications call contracts without code

SB L54: Subscribing multiple times to contracts without code

SB M78: Unsubscribe should have a Minimum Gas Limit

Description of changes

  • if subscriber has no code then unsubscribe will not revert
  • Added a custom error for unsubscribing when already unsubscribed
  • if subscriber has no code then notifying subscribe, transfer, and modifyLiquidity will revert
  • Unsubscribe requires a minimum gas limit otherwise revert. before users can use pick a gas limit where notifyUnsubscribe reverts (and is try/catched) with OutOfGas but the overall transaction still succeeds

@saucepoint saucepoint changed the title OZ-L10: Check code length on Unsubscribe OZ-L10: Check code length on Notifications Aug 30, 2024
hensha256
hensha256 previously approved these changes Aug 30, 2024
@saucepoint saucepoint changed the title OZ-L10: Check code length on Notifications OZ-L10, SB-78; Check code length on Notifications, Unsubscribe Minimum Gas Limit Aug 30, 2024
@saucepoint saucepoint changed the title OZ-L10, SB-78; Check code length on Notifications, Unsubscribe Minimum Gas Limit OZ-L10, SB-L53, SB-L54, SB-M78; Check code length on Notifications, Unsubscribe Minimum Gas Limit Aug 30, 2024
Comment on lines 77 to 82
// require that the remaining gas is sufficient to notify the subscriber
// otherwise, users can select a gas limit where .notifyUnsubscribe hits OutOfGas yet the transaction/unsubscription
// can still succeed
// to account for EIP-150, condition could be 64 * gasleft() / 63 <= subscriberGasLimit
if ((64 * gasleft() / 63) < subscriberGasLimit) GasLimitTooLow.selector.revertWith();
try _subscriber.notifyUnsubscribe{gas: subscriberGasLimit}(tokenId, config, data) {} catch {}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

need extra eyes on this (and the natspec)

feels weirdly unintuitive that users may need to specify a very high gas limit on .unsubscribe() calls

@snreynolds @hensha256

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah it means that all users will have to send in gasLimit > subscriberGasLimit, even if their particular subsciber only needs like 10k in gas. Which sucks really... but I can't see any other way around it?
I think it also means that it will be really hard to run estimateGas because of this require statement.

// otherwise, users can select a gas limit where .notifyUnsubscribe hits OutOfGas yet the transaction/unsubscription
// can still succeed
// to account for EIP-150, condition could be 64 * gasleft() / 63 <= subscriberGasLimit
if ((64 * gasleft() / 63) < subscriberGasLimit) GasLimitTooLow.selector.revertWith();
Copy link
Contributor

Choose a reason for hiding this comment

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

in core we dont bother with the *64/63. Theres just a comment about it
https://github.com/Uniswap/v4-core/blob/4dc48bbd89f4faf1e9493cd1563908d79aa10b0d/src/ProtocolFees.sol#L76

thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

agree

src/interfaces/INotifier.sol Outdated Show resolved Hide resolved
@@ -10,6 +10,9 @@ interface ISubscriber {
/// @param config details about the position
/// @param data additional data passed in by the caller
function notifySubscribe(uint256 tokenId, PositionConfig memory config, bytes memory data) external;
/// @notice Called when a position unsubscribes from the subscriber
/// @dev This call's gas is capped at `unsubscribeGasLimit` (set at deployment)
/// @dev Because of EIP-150, solidity may only allocate 63/64 of gasleft()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// @dev Because of EIP-150, solidity may only allocate 63/64 of gasleft()
/// @dev Because of EIP-150, solidity may only allocate 63/64 of unsubscribeGasLimit

Copy link
Member

Choose a reason for hiding this comment

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

i actually think its clearer to leave it as

/// @dev Because of EIP-150, solidity may only allocate 63/64 of gasleft()

because this only happens when gasLeft == unsubscriberGasLimit

Copy link
Contributor

Choose a reason for hiding this comment

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

i just mean that subscribers should know that they actually cannot take more than 63/64 of unsubscribeGasLimit because thats the lower bound of gas they will receive

Copy link
Member

Choose a reason for hiding this comment

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

yeah agree that is the lower bound !

@@ -65,13 +66,17 @@ abstract contract Notifier is INotifier {
function _unsubscribe(uint256 tokenId, PositionConfig calldata config) internal {
_positionConfigs(tokenId).setUnsubscribe();
ISubscriber _subscriber = subscriber[tokenId];
if (_subscriber == NO_SUBSCRIBER) NotSubscribed.selector.revertWith();
Copy link
Contributor

Choose a reason for hiding this comment

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

i see that this check makes sense, surprised it wasnt here before
doesnt one of these issues say to add it? i cant find it

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 it may make more sense to check _postionConfigs().hasSubscriber? i guess see if its cheaper in the case where they do have a subscriber?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep looked like .hasSubscriber() was slightly cheaper! refactored it

src/base/Notifier.sol Outdated Show resolved Hide resolved
if ((64 * gasleft() / 63) < subscriberGasLimit) GasLimitTooLow.selector.revertWith();
try _subscriber.notifyUnsubscribe{gas: subscriberGasLimit}(tokenId, config) {} catch {}
// to account for EIP-150, condition could be 64 * gasleft() / 63 <= unsubscribeGasLimit
if ((64 * gasleft() / 63) < unsubscribeGasLimit) GasLimitTooLow.selector.revertWith();
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 we do not need to calculate the (64 * gasLeft / 63) check.

Really this line is just being added to enforce some minimum amount that a user MUST send.

That minimum amount doesn't exactly need to be 63/64ths of the unsubscribeGasLimit. It can just be at least the unsubscribeGasLimit. And we can add a comment that in 1 case (when gasLeft == gasLimit) the call will only forward 63/64ths of the gasLimit... which is fine. Basically we can just keep this in mind when parameterizing the unsubscribeGasLimit.

We really want to add this line because we do not want the user to ever send a minimal amount of gas such that the unsubscribe block reverts but the rest of the function succeeds

Copy link
Member

@snreynolds snreynolds left a comment

Choose a reason for hiding this comment

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

I see that we changed it to document EIP150. I just left some comments in response to Alice's about the natspec!

Looks good!

// a percentage of the block.gaslimit denoted in BPS, used as the gas limit for subscriber calls
// 100 bps is 1%, at 30M gas, the limit is 300K
uint256 private constant BLOCK_LIMIT_BPS = 100;
/// @inheritdoc INotifier
Copy link
Member

Choose a reason for hiding this comment

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

nit: i think there is an extra line added that can be removed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this natspec? fwiw i added this to INotifier

    /// @notice Returns and determines the maximum allowable gas-used for notifying unsubscribe
    /// @return uint256 the maximum gas limit when notifying a subscriber's `notifyUnsubscribe` function
    function unsubscribeGasLimit() external view returns (uint256);

@@ -65,13 +66,17 @@ abstract contract Notifier is INotifier {
function _unsubscribe(uint256 tokenId, PositionConfig calldata config) internal {
_positionConfigs(tokenId).setUnsubscribe();
ISubscriber _subscriber = subscriber[tokenId];
if (_subscriber == NO_SUBSCRIBER) NotSubscribed.selector.revertWith();
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 it may make more sense to check _postionConfigs().hasSubscriber? i guess see if its cheaper in the case where they do have a subscriber?

@@ -10,6 +10,9 @@ interface ISubscriber {
/// @param config details about the position
/// @param data additional data passed in by the caller
function notifySubscribe(uint256 tokenId, PositionConfig memory config, bytes memory data) external;
/// @notice Called when a position unsubscribes from the subscriber
/// @dev This call's gas is capped at `unsubscribeGasLimit` (set at deployment)
/// @dev Because of EIP-150, solidity may only allocate 63/64 of gasleft()
Copy link
Member

Choose a reason for hiding this comment

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

i actually think its clearer to leave it as

/// @dev Because of EIP-150, solidity may only allocate 63/64 of gasleft()

because this only happens when gasLeft == unsubscriberGasLimit

snreynolds
snreynolds previously approved these changes Sep 4, 2024
Copy link
Member

@snreynolds snreynolds left a comment

Choose a reason for hiding this comment

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

nice

@hensha256 hensha256 merged commit 255b20e into main Sep 4, 2024
3 checks passed
@hensha256 hensha256 deleted the oz-L10-unsubscribe-revert branch September 4, 2024 20:14
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