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

Introducing PolygonTokenHook: An Asynchronous Hook for Verifying Polygon Token Ownership #217

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions contracts/hooks/PolygonTokenHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import { HookResult } from "contracts/interfaces/hooks/base/IHook.sol";
import { AsyncBaseHook } from "contracts/hooks/base/AsyncBaseHook.sol";
import { Errors } from "contracts/lib/Errors.sol";
import { PolygonToken } from "contracts/lib/hooks/PolygonToken.sol";
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

/// @title PolygonTokenHook
/// @notice This is asynchronous hook used to verify a user owning specific Polygon tokens.
contract PolygonTokenHook is AsyncBaseHook {
/// @notice The address that is allowed to call the callback function.
/// @dev This address is set during contract deployment and cannot be changed afterwards.
address private immutable CALLBACK_CALLER;

/// @notice A counter used to generate unique request IDs for each token request.
uint256 private nonce;

/// @notice A mapping that links each request ID to a PolygonTokenRequest struct.
mapping(bytes32 => PolygonTokenRequest) private requestIdToRequest;

/// @notice A struct used to store information about a token request.
/// @dev It includes the requester's address, the token's address, the token owner's address, a balance threshold,
/// and two boolean flags to indicate whether the request is completed and whether it exists.
struct PolygonTokenRequest {
address requester;
address tokenAddress;
address tokenOwnerAddress;
uint256 balanceThreshold;
bool isRequestCompleted;
bool exists;
}

/// @notice Emits an event for a Polygon token balance request.
/// @param requestId The unique ID of the request.
/// @param requester The address of the requester.
/// @param tokenAddress The address of the token.
/// @param tokenOwnerAddress The address of the token owner.
/// @param callbackAddr The address of the callback.
/// @param callbackFunctionSignature The signature of the callback function.

event PolygonTokenBalanceRequest(
bytes32 indexed requestId,
address indexed requester,
address tokenAddress,
address tokenOwnerAddress,
address callbackAddr,
bytes4 callbackFunctionSignature
);

/// @notice Initializes the contract during deployment.
/// @param accessControl_ The address of the access control contract.
/// @param callbackCaller_ The address of the callback caller contract.
constructor(
address accessControl_,
address callbackCaller_
) AsyncBaseHook(accessControl_) {
if (callbackCaller_ == address(0)) revert Errors.ZeroAddress();
CALLBACK_CALLER = callbackCaller_;
}

/// @notice Handles the callback of a token request.
/// @param requestId The unique ID of the request.
/// @param balance The balance of the token.
function handleCallback(bytes32 requestId, uint256 balance) external {
bool isPassed = false;
string memory errorMessage = "";
require(requestIdToRequest[requestId].exists, "Request not found");
if (balance < requestIdToRequest[requestId].balanceThreshold) {
errorMessage = "Followers count is not enough";
} else {
isPassed = true;
}
delete requestIdToRequest[requestId];
_handleCallback(requestId, abi.encode(isPassed, errorMessage));
}

/// @notice Validates the configuration for the hook.
/// @dev This function checks if the tokenAddress and balanceThreshold in the configuration are valid.
/// It reverts if the tokenAddress is the zero address or if the balanceThreshold is zero.
/// @param hookConfig_ The configuration data for the hook, encoded as bytes.
function _validateConfig(bytes memory hookConfig_) internal pure override {
PolygonToken.Config memory config = abi.decode(
hookConfig_,
(PolygonToken.Config)
);
if (config.tokenAddress == address(0)) {
revert Errors.Hook_InvalidHookConfig("tokenAddress is 0");
}
if (config.balanceThreshold == 0) {
revert Errors.Hook_InvalidHookConfig("balanceThreshold is 0");
}
}

/// @dev Internal function to request an asynchronous call,
/// concrete hoot implementation should override the function.
/// The function should revert in case of error.
/// @param hookConfig_ The configuration of the hook.
/// @param hookParams_ The parameters for the hook.
/// @return hookData The data returned by the hook.
/// @return requestId The ID of the request.
function _requestAsyncCall(
bytes memory hookConfig_,
bytes memory hookParams_
) internal override returns (bytes memory hookData, bytes32 requestId) {
PolygonToken.Config memory config = abi.decode(
hookConfig_,
(PolygonToken.Config)
);
PolygonToken.Params memory params = abi.decode(
hookParams_,
(PolygonToken.Params)
);
requestId = keccak256(abi.encodePacked(this, nonce++));
hookData = "";

requestIdToRequest[requestId] = PolygonTokenRequest({
requester: msg.sender,
tokenAddress: config.tokenAddress,
tokenOwnerAddress: params.tokenOwnerAddress,
balanceThreshold: config.balanceThreshold,
isRequestCompleted: false,
exists: true
});

emit PolygonTokenBalanceRequest(
requestId,
msg.sender,
config.tokenAddress,
params.tokenOwnerAddress,
address(this),
this.handleCallback.selector
);
}

/// @notice Returns the address of the callback caller.
/// @param requestId The unique ID of the request.
/// @return The address of the callback caller.
function _callbackCaller(bytes32) internal view override returns (address) {
return CALLBACK_CALLER;
}
}
9 changes: 8 additions & 1 deletion contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ library Errors {

error BaseModule_HooksParamsLengthMismatch(uint8 hookType);
error BaseModule_ZeroIpaRegistry();
error BaseModule_ZeroModuleRegistry();
error BaseModule_ZeroLicenseRegistry();
error BaseModule_OnlyModuleRegistry();

Expand All @@ -60,6 +59,8 @@ library Errors {
error HookRegistry_HooksConfigLengthMismatch();
/// @notice This error is thrown when the provided index is out of bounds of the hooks array.
error HookRegistry_IndexOutOfBounds(uint256 hooksIndex);
error HookRegistry_ZeroModuleRegistry();


////////////////////////////////////////////////////////////////////////////
// BaseRelationshipProcessor //
Expand All @@ -74,6 +75,7 @@ library Errors {

error ModuleRegistry_ModuleNotRegistered(string moduleName);
error ModuleRegistry_CallerNotOrgOwner();
error ModuleRegistry_HookNotRegistered(string hookKey);

////////////////////////////////////////////////////////////////////////////
// CollectModule //
Expand Down Expand Up @@ -403,6 +405,11 @@ library Errors {
/// @notice The address is not the owner of the token.
error TokenGatedHook_NotTokenOwner(address tokenAddress, address ownerAddress);

error Hook_AsyncHookError(bytes32 requestId, string reason);

/// @notice Invalid Hook configuration.
error Hook_InvalidHookConfig(string reason);

////////////////////////////////////////////////////////////////////////////
// LicensorApprovalHook //
////////////////////////////////////////////////////////////////////////////
Expand Down
12 changes: 11 additions & 1 deletion contracts/lib/hooks/Hook.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.19;

import { IHook } from "contracts/interfaces/hooks/base/IHook.sol";
/// @title Hook
/// @notice This library defines the ExecutionContext struct used when executing hooks.
/// @dev The ExecutionContext struct contains two fields: config and params, both of type bytes.
Expand All @@ -16,4 +16,14 @@ library Hook {
/// @dev These parameters are passed from the external caller when executing modules.
bytes params;
}

function canSupportSyncCall(IHook self) internal pure returns (bool) {
// TODO: return uint256(uint160(address(self))) & SYNC_FLAG != 0;
return true;
}

function canSupportAsyncCall(IHook self) internal pure returns (bool) {
// TODO: return uint256(uint160(address(self))) & ASYNC_FLAG != 0;
return true;
}
}
24 changes: 24 additions & 0 deletions contracts/lib/hooks/PolygonToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.19;

/// @title TokenGated
/// @notice This library defines the Config and Params structs used in the TokenGatedHook.
/// @dev The Config struct contains the tokenAddress field, and the Params struct contains the tokenOwner field.
library PolygonToken {
/// @notice Defines the required configuration information for the TokenGatedHook.
/// @dev The Config struct contains a single field: tokenAddress.
struct Config {
/// @notice The threshold of number of users who follow this user.
address tokenAddress;
/// @notice The threshold of number of lists that include this user.
uint256 balanceThreshold;
}

/// @notice Defines the required parameter information for executing the TokenGatedHook.
/// @dev The Params struct contains a single field: tokenOwner.
struct Params {
/// @notice The address of the token owner.
/// @dev This address is checked against the tokenAddress in the Config struct to ensure the owner has a token.
address tokenOwnerAddress;
}
}
104 changes: 93 additions & 11 deletions contracts/modules/ModuleRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,26 @@ import { Errors } from "contracts/lib/Errors.sol";
import { IIPOrg } from "contracts/interfaces/ip-org/IIPOrg.sol";
import { BaseModule } from "./base/BaseModule.sol";
import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol";
import { IHook } from "contracts/interfaces/hooks/base/IHook.sol";

/// @title ModuleRegistry
/// @notice This contract is the source of truth for all modules that are registered in the protocol.
/// It's also the entrypoint for execution and configuration of modules, either directly by users
/// or by MODULE_EXECUTOR_ROLE holders.
contract ModuleRegistry is IModuleRegistry, AccessControlled, Multicall {

address public constant PROTOCOL_LEVEL = address(0);

mapping(string => BaseModule) internal _protocolModules;
mapping(string => IHook) internal _protocolHooks;
mapping(IHook => string) internal _hookKeys;

constructor(address accessControl_) AccessControlled(accessControl_) { }
constructor(address accessControl_) AccessControlled(accessControl_) {}

/// @notice Gets the protocol-wide module associated with a module key.
/// @param moduleKey_ The unique module key used to identify the module.
function protocolModule(string calldata moduleKey_) public view returns (address) {
function protocolModule(
string calldata moduleKey_
) public view returns (address) {
return address(_protocolModules[moduleKey_]);
}

Expand Down Expand Up @@ -58,15 +62,56 @@ contract ModuleRegistry is IModuleRegistry, AccessControlled, Multicall {
}

/// Get a module from the protocol, by its key.
function moduleForKey(string calldata moduleKey) external view returns (BaseModule) {
function moduleForKey(
string calldata moduleKey
) external view returns (BaseModule) {
return _protocolModules[moduleKey];
}

// Returns true if the provided address is a module.
function isModule(string calldata moduleKey, address caller_) external view returns (bool) {
function isModule(
string calldata moduleKey,
address caller_
) external view returns (bool) {
return address(_protocolModules[moduleKey]) == caller_;
}

function registerProtocolHook(
string calldata hookKey,
IHook hookAddress
) external onlyRole(AccessControl.MODULE_REGISTRAR_ROLE) {
if (address(hookAddress) == address(0)) {
revert Errors.ZeroAddress();
}
_protocolHooks[hookKey] = hookAddress;
_hookKeys[hookAddress] = hookKey;
// emit HookAdded(PROTOCOL_LEVEL, hookKey, address(hookAddress));
}

function removeProtocolHook(
string calldata hookKey
) external onlyRole(AccessControl.MODULE_REGISTRAR_ROLE) {
if (address(_protocolHooks[hookKey]) == address(0)) {
revert Errors.ModuleRegistry_HookNotRegistered(hookKey);
}
IHook hookAddress = _protocolHooks[hookKey];
delete _protocolHooks[hookKey];
delete _hookKeys[hookAddress];
// emit HookRemoved(PROTOCOL_LEVEL, hookKey, hookAddress);
}

function hookForKey(string calldata hookKey) external view returns (IHook) {
return _protocolHooks[hookKey];
}

function hookKey(IHook hookAddress) external view returns (string memory) {
return _hookKeys[hookAddress];
}

function isRegisteredHook(IHook hook_) external view returns (bool) {
return address(_protocolHooks[_hookKeys[hook_]]) == address(hook_);
}

/// Execution entrypoint, callable by any address on its own behalf.
/// @param ipOrg_ address of the IPOrg, or address(0) for protocol-level stuff
/// @param moduleKey_ short module descriptor
Expand All @@ -81,7 +126,15 @@ contract ModuleRegistry is IModuleRegistry, AccessControlled, Multicall {
bytes[] memory preHookParams_,
bytes[] memory postHookParams_
) external returns (bytes memory) {
return _execute(ipOrg_, msg.sender, moduleKey_, moduleParams_, preHookParams_, postHookParams_);
return
_execute(
ipOrg_,
msg.sender,
moduleKey_,
moduleParams_,
preHookParams_,
postHookParams_
);
}

/// Execution entrypoint, callable by any MODULE_EXECUTOR_ROLE holder on behalf of any address.
Expand All @@ -99,8 +152,20 @@ contract ModuleRegistry is IModuleRegistry, AccessControlled, Multicall {
bytes calldata moduleParams_,
bytes[] calldata preHookParams_,
bytes[] calldata postHookParams_
) external onlyRole(AccessControl.MODULE_EXECUTOR_ROLE) returns (bytes memory) {
return _execute(ipOrg_, caller_, moduleKey_, moduleParams_, preHookParams_, postHookParams_);
)
external
onlyRole(AccessControl.MODULE_EXECUTOR_ROLE)
returns (bytes memory)
{
return
_execute(
ipOrg_,
caller_,
moduleKey_,
moduleParams_,
preHookParams_,
postHookParams_
);
}

/// Configuration entrypoint, callable by any address on its own behalf.
Expand All @@ -125,7 +190,11 @@ contract ModuleRegistry is IModuleRegistry, AccessControlled, Multicall {
address caller_,
string calldata moduleKey_,
bytes calldata params_
) external onlyRole(AccessControl.MODULE_EXECUTOR_ROLE) returns (bytes memory) {
)
external
onlyRole(AccessControl.MODULE_EXECUTOR_ROLE)
returns (bytes memory)
{
return _configure(ipOrg_, caller_, moduleKey_, params_);
}

Expand All @@ -141,8 +210,21 @@ contract ModuleRegistry is IModuleRegistry, AccessControlled, Multicall {
if (address(module) == address(0)) {
revert Errors.ModuleRegistry_ModuleNotRegistered(moduleKey_);
}
result = module.execute(ipOrg_, caller_, moduleParams_, preHookParams_, postHookParams_);
emit ModuleExecuted(address(ipOrg_), moduleKey_, caller_, moduleParams_, preHookParams_, postHookParams_);
result = module.execute(
ipOrg_,
caller_,
moduleParams_,
preHookParams_,
postHookParams_
);
emit ModuleExecuted(
address(ipOrg_),
moduleKey_,
caller_,
moduleParams_,
preHookParams_,
postHookParams_
);
return result;
}

Expand Down
Loading
Loading