Skip to content

Commit

Permalink
Introduce PolygonTokenHook and integrate Async hook with Moudle
Browse files Browse the repository at this point in the history
  • Loading branch information
kingster-will committed Dec 7, 2023
1 parent d0ed02c commit 8f0b80b
Show file tree
Hide file tree
Showing 27 changed files with 1,238 additions and 198 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ SEPOLIA_ADMIN_ADDRESS = 0x12341234123412341234123412341234
ETHERSCAN_API_KEY = ETHERSCANAPIKEYETHERSCANAPIKEY

# PROTOCOL LICENSE URL
PIPL_URL=https://url-to-license-file.pdf
PIPL_URL=https://url-to-license-file.pdf

# POLYGON TOKEN ORACLE
POLYGON_TOKEN_ORACLE_CLIENT=0x123412341234123412341234123412341234
POLYGON_TOKEN_ORACLE_COORDINATOR=0x123412341234123412341234123412341234
4 changes: 4 additions & 0 deletions contracts/StoryProtocol.sol
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ contract StoryProtocol is Multicall {
preHooksData_,
postHooksData_
);
// If the result is empty, then the registration module is pending for async hook execution.
if (result.length == 0) {
return (0, 0);
}
return abi.decode(result, (uint256, uint256));
}

Expand Down
146 changes: 146 additions & 0 deletions contracts/hooks/PolygonTokenHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// 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";

interface IPolygonTokenClient {
function sendRequest(
bytes32 requestId,
address requester,
address tokenAddress,
address tokenOwnerAddress,
address callbackAddr,
bytes4 callbackFunctionSignature
) external;
}

/// @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;

address public immutable ORACLE_CLIENT;

/// @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 Initializes the contract during deployment.
/// @param accessControl_ The address of the access control contract.
/// @param oracleClient_ The address of the oracle client contract for access Polygon Token info.
/// @param callbackCaller_ The address of the callback caller contract.
constructor(
address accessControl_,
address oracleClient_,
address callbackCaller_
) AsyncBaseHook(accessControl_) {
if (callbackCaller_ == address(0)) revert Errors.ZeroAddress();
if (oracleClient_ == address(0)) revert Errors.ZeroAddress();
CALLBACK_CALLER = callbackCaller_;
ORACLE_CLIENT = oracleClient_;
}

/// @notice Handles the callback of a token request.
/// @param requestId The unique ID of the request.
/// @param balance The balance of the token.
/// @dev This function checks if the request exists and verifies the token balance against the balance threshold.
/// If the balance is less than the threshold, it sets an error message. Otherwise, it sets the isPassed flag to true.
/// It then deletes the request from the requestIdToRequest mapping.
/// Finally, it calls the _handleCallback() function, passing the requestId and the encoded isPassed flag and errorMessage.
/// The encoding is done using abi.encode(isPassed, errorMessage).
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 = "Balance of Token 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
});

IPolygonTokenClient(ORACLE_CLIENT).sendRequest(
requestId,
msg.sender,
config.tokenAddress,
params.tokenOwnerAddress,
address(this),
this.handleCallback.selector
);
}

/// @notice Returns the address of the callback caller.
/// @return The address of the callback caller.
function _callbackCaller(bytes32) internal view override returns (address) {
return CALLBACK_CALLER;
}
}
14 changes: 14 additions & 0 deletions contracts/interfaces/modules/IModuleRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ interface IModuleRegistry {
bytes params
);

/// @notice Emits when a new hook is added for a specific IP Org.
event HookAdded(
address indexed ipOrg,
string hookKey,
address indexed hook
);

/// @notice Emits when a hook is removed for an IP Org.
event HookRemoved(
address indexed ipOrg,
string hookKey,
address indexed hook
);

/// @notice Fetches the latest protocol module bound to a specific key.
function protocolModule(string calldata moduleKey) external view returns (address);
}
4 changes: 2 additions & 2 deletions contracts/interfaces/modules/base/IModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { IIPOrg } from "contracts/interfaces/ip-org/IIPOrg.sol";
/// @title IModule
/// @notice Interface for a Story Protocol Module, building block of the protocol functionality.
interface IModule {

/// The execution of the module is pending, and will need to be executed again.
event RequestPending(address indexed sender);
/// Module execution completed successfully.
event RequestCompleted(address indexed sender);
/// Module execution failed.
event RequestFailed(address indexed sender, string reason);

/// @notice Main execution entrypoint.
/// @dev It will verify params, execute pre action hooks, perform the action,
Expand Down Expand Up @@ -43,5 +44,4 @@ interface IModule {
address caller_,
bytes calldata params_
) external returns (bytes memory result);

}
10 changes: 9 additions & 1 deletion contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ library Errors {

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

Expand All @@ -58,6 +57,9 @@ 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();
error HookRegistry_RegisteringNonWhitelistedHook(address hookAddress);


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

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

////////////////////////////////////////////////////////////////////////////
// CollectModule //
Expand Down Expand Up @@ -409,6 +412,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
22 changes: 21 additions & 1 deletion contracts/lib/hooks/Hook.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// SPDX-License-Identifier: UNLICENSED
// See Story Protocol Alpha Agreement: https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf
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.
library Hook {
uint256 internal constant SYNC_FLAG = 1 << 159;
uint256 internal constant ASYNC_FLAG = 1 << 158;
/// @notice Defines the execution context for a hook.
/// @dev The ExecutionContext struct is used as a parameter when executing hooks.
struct ExecutionContext {
Expand All @@ -17,4 +19,22 @@ library Hook {
/// @dev These parameters are passed from the external caller when executing modules.
bytes params;
}

/// @notice Checks if the hook can support synchronous calls.
/// @dev This function checks if the first bit of the hook address is set to 1,
/// indicating that the hook can support synchronous calls.
/// @param self_ The hook to check.
/// @return A boolean indicating if the hook can support synchronous calls.
function canSupportSyncCall(IHook self_) internal pure returns (bool) {
return uint256(uint160(address(self_))) & SYNC_FLAG != 0;
}

/// @notice Checks if the hook can support asynchronous calls.
/// @dev This function checks if the second bit of the hook address is set to 1,
/// indicating that the hook can support asynchronous calls.
/// @param self_ The hook to check.
/// @return A boolean indicating if the hook can support asynchronous calls.
function canSupportAsyncCall(IHook self_) internal pure returns (bool) {
return uint256(uint160(address(self_))) & ASYNC_FLAG != 0;
}
}
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: MIT
pragma solidity ^0.8.19;

/**
* @title PolygonToken
* @dev This library is used for managing Polygon tokens.
*/
library PolygonToken {
/// @notice This is the configuration for the Polygon token.
/// @dev It includes the token address and the balance threshold.
struct Config {
/// @notice The address of the Polygon token.
address tokenAddress;
/// @notice The balance threshold for the Polygon token.
uint256 balanceThreshold;
}

/// @notice This is the parameters for the Polygon token.
/// @dev It includes the token owner address.
struct Params {
/// @notice The address of the Polygon token owner.
address tokenOwnerAddress;
}
}
49 changes: 49 additions & 0 deletions contracts/modules/ModuleRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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.
Expand All @@ -19,6 +20,8 @@ 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_) { }

Expand Down Expand Up @@ -58,6 +61,38 @@ contract ModuleRegistry is IModuleRegistry, AccessControlled, Multicall {
emit ModuleRemoved(PROTOCOL_LEVEL, moduleKey, moduleAddress);
}

/// @notice Registers a new protocol hook.
/// @param hookKey The unique identifier for the hook.
/// @param hookAddress The address of the hook contract.
/// @dev This function can only be called by an account with the MODULE_REGISTRAR_ROLE.
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));
}

/// @notice Removes a protocol hook.
/// @param hookKey The unique identifier for the hook.
/// @dev This function can only be called by an account with the MODULE_REGISTRAR_ROLE.
/// If the hook is not registered, it reverts with an error.
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, address(hookAddress));
}

/// Get a module from the protocol, by its key.
function moduleForKey(string calldata moduleKey) external view returns (BaseModule) {
return _protocolModules[moduleKey];
Expand All @@ -68,6 +103,20 @@ contract ModuleRegistry is IModuleRegistry, AccessControlled, Multicall {
return address(_protocolModules[moduleKey]) == caller_;
}

/// @notice Returns the protocol hook associated with a given hook key.
/// @param hookKey The unique identifier for the hook.
/// @return The protocol hook associated with the given hook key.
function hookForKey(string calldata hookKey) external view returns (IHook) {
return _protocolHooks[hookKey];
}

/// @notice Checks if a hook is registered in the protocol.
/// @param hook_ The hook to check.
/// @return True if the hook is registered, false otherwise.
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 Down
Loading

0 comments on commit 8f0b80b

Please sign in to comment.