Move Paused and Unpaused event definitions to an interface
🧐 Motivation
Currently, Paused and Unpaused events are hardcoded in contracts/utils/Pausable.sol and contracts/utils/PausableUpgradeable.sol. While these events are included in the ABI after compilation, customizing ABI visibility for different user groups—so they only load relevant parts into their wallets—requires manual editing of the ABI JSON.
Ideally, different interface files could be defined for different views and linked via inheritance. However, because these events are embedded directly in the implementation contracts, they cannot be reused in interface files without causing Event with same name and parameter types defined twice error.. This is due to Solidity's restriction against redeclaring events.
📝 Details
To improve modularity and ABI customization, I propose moving the Paused and Unpaused events into a dedicated interface. Then, both Pausable.sol and PausableUpgradeable.sol can implement this interface.
This change would enable developers to selectively expose these events through interfaces without requiring manual modification of the ABI.
Overall, this idea can be applied to all errors and events embedded in implementation contracts like contracts-upgradeable/contracts/proxy/utils/Initializable.sol and contracts-upgradeable/utils/PausableUpgradeable.sol
Hello @dilumb
This change would enable developers to selectively expose these events through interfaces without requiring manual modification of the ABI.
I'm not sure I understand what this means. Can you provide a concrete example of what "selectively expose events" means and why its desirable ?
TL/DR - It's useful to move errors and events out of an implementation contract, so that they can be reused (not redefined) in other contracts or interfaces.
@Amxx, the underlying situation is a bit more involved. I'll try to explain.
We have a set of contracts that various parties/roles interact with. Parties utilise wallets that enable ABI uploading to streamline interactions and minimise errors. While all parties should be aware of all errors and events, not all parties need to load all functions to their wallet UI. This is for better user experience and separation of duties at UI level, rather than hiding/security.
For instance, pause function (which sits in a contract that inherits Pausable.sol and calls _pause) is only required by the admin role. But other roles should be able to listen to Paused and Unpaused events defined in the inherited contract Pausable.sol. If I give non-admin users the full ABI, their wallet shows the pause function, which they don't need to engage with.
If those events and errors were defined outside the implementation as an interface, I could have just imported that interface to build a custom interface with all events, errors, and only the required functions. Then compile that interface to get the ABI. Also, it can be linked in the overall interface hierarchy so that the compiler will check against full contract implementation.
I can't redeclare an interface file for those events/errors, as it'll cause Event with same name and parameter types defined twice error. My only option is to define an interface and use it to generate ABI, without linking those to the overall interface hierarchy to be validated by the compiler against implementation. I essentially have hanging interfaces that aren't validated.
I'm confused by what exactly you are asking.
What I understand is that you want multiple entities to have different views of a contracts (I personnally don't think this should be handled at the ABI level, but for the sake of discussion lets go with that).
Lets take an example where you have a contract C that is Ownable, ERC20, Pausable. C also exposes pause and unpause functions that have the onlyOwner modifier. The ABI to C is the "sum" of all its components ABIs plus whatever C defines itself.
Lets say you'd like you user to get an ABI that includes all of IERC20, and the relevant part of Pausable, but not the public pause and unpause functions that you added, or any of the Ownable logic.
If I wanted to do that, I'd already have 2 options:
- use the compiler to build it for me using an helper contract
abstract contract MyFakeInterface is IERC20Metadata, Pausable {}
contract MyRealContract is MyFakeInterface, ERC20, Ownable, Pausable {
[...]
function pause() public onlyOwner() { _pause(); }
function unpause() public onlyOwner() { _unpause(); }
}
When you compile that, you'd get an ABI for MyFakeInterface that contains the event and error from Pausable, as well as the paused() getter. I would NOT include the pause() and unpause function definitions.
- do that without the compiler, by concatenating the abi present in
@openzeppelin/contracts/build/contracts/IERC20Metadata.jsonand@openzeppelin/contracts/build/contracts/Pausable.json
Both methods should give you the same result.
I'm not sure how an interface would help here. What would be in that interface ? If the proposed interface includes:
- the 2 events
- the 2 errors
- the
paused()getter
that are in Pausable ... then why not dirrectly use Pausable ?
Option 1 leads to Linearization of inheritance graph impossible errors. See the inheritance graph for your example:
In our case, a few more contracts are involved, making inheritance hierarchy even more complicated.
Option 2 is what we ended up doing. But it's suboptimal as the compiler doesn't check the implementation with interfaces.