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 Polygon Token Async Hook and Integration Async Hook with BaseModule #230

Merged
merged 2 commits into from
Dec 7, 2023
Merged
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
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
1,495 changes: 841 additions & 654 deletions broadcast/Main.s.sol/11155111/run-latest.json

Large diffs are not rendered by default.

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