diff --git a/README.md b/README.md index 5244996..b01f849 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,26 @@ - a creator attaches this module to one of their posts and specifies parameters such as the destination chain id - any user that collects the post gets an OmniSBT minted on the destination chain -## blog post describing this project in detail [here](./blog/blog.md) +`LZGatedFollowModule` is a Lens Follow Module that allows profile holders to gate their following with ERC20 or ERC721 balances held on other chains +- a profile initializes this module with a supported LayerZero chain id and a ERC20/ERC721 contract + balance to check against +- another user wishes to follow, they read the requirements using `gatedFollowPerProfile` which points to the chain/token/balance +- user generates `followWithSigData` for a given profile +- user (or relayer) submits this sig along with appropriate input to the `LZGatedProxy` contract on the other chain +- `LZGatedProxy` makes the token check, and relays the message to the `LZGatedFollowModule` on polygon +- `LZGatedFollowModule` contract validates the input, submits sig to `LensHub#followWithSig` +- follow ✅ + +`LZGatedReferenceModule` is a Lens Reference Module that allows publication creators to gate who can comment/mirror their post with ERC20 or ERC721 balances held on other chains. + +`LZGatedCollectModule` is a Lens Collect Module that allows publication creators to gate who can collect their post with ERC20 or ERC721 balances held on other chains. + +`LZGatedProxy.sol` acts acts as a proxy for `LZGatedFollowModule`, `LZGatedReferenceModule`, and `LZGatedCollectModule` in order to read token balances from remote contracts on any chain supported by LayerZero. +- we deploy one of these on any EVM chain to be supported by the modules, wire up for trusted LayerZero messaging +- when a user wishes to follow/comment/mirror/collect - they sign the correct data payload and submit along with the correct gate data +- the contract makes the token balance check against the ERC20/ERC721 contract defined +- if all good, relay the payload to our module contract on polygon + +## blog post describing the `LZCollectModule` in detail [here](./blog/blog.md) ## setup + compile contracts ``` @@ -33,18 +52,34 @@ yarn test yarn coverage ``` -## deploying contracts -We deploy our `OmniSBT` contract on the destination chain (`fantom_testnet`) and the source chain (`mumbai`), as well as setting trusted remotes on both ends. Finally, we stub the `FollowCampaignModule` to be the deployer address +## deploying contracts (OmniSBT + LZCollectModule) +We deploy our `OmniSBT` contract on the destination chain (`fantom_testnet`) and the source chain (`mumbai`), as well as setting trusted remotes on both ends. Finally, we stub the `FollowCampaignModule` to be the deployer address. All the lz config can be found under `tasks/helpers/constants.ts` 1. deploy `OmniSBT` contract on the destination chain `npx hardhat deploy-token-remote --network fantom_testnet` + 2. deploy `OmniSBT` contract on the source chain + set trusted remote `npx hardhat deploy-token-source --network mumbai` + 3. set trusted remote on the destination chain `npx hardhat set-trusted-remote --network fantom_testnet` -## stubbed transactions + tenderly infra +## deploying contracts (LZGated* modules) +We deploy our Lens modules on the source chain (`mumbai`) and we deploy our `LZGatedProxy` contract to all the remote chains we want to support (`goerli` and `optimismTestnet`). Finally, we set the trusted remotes on each module. All the lz config can be found under `tasks/helpers/constants.ts` +1. deploy our modules on the source chain `npx hardhat deploy-modules-source --network mumbai` +2. deploy our `LZGatedProxy` contract on all remote chains + + a. `npx hardhat deploy-proxy-remote --network goerli` + + b. `npx hardhat deploy-proxy-remote --network optimismTestnet` +3. set our trusted remotes on the source chain `npx hardhat set-trusted-remotes --network mumbai` + +## stubbed transactions + tenderly infra (OmniSBT + LZCollectModule) Considering that our `LZCollectModule` contract is what triggers mints of `OmniSBT` and it relies on lens module whitelisting - we can stub those transaction by setting the collect module to some permissioned address and triggering mints from an off-chain process 1. create a collection and set our lens testnet address as the collect module `npx hardhat stub-create-collection --network mumbai` + 2. create a post that we will listen for collects on https://testnet.lenster.xyz/ + 3. address the `@TODO` in `processCollected.ts` and deploy our tenderly action `cd tenderly && npm run deploy` - NOTE: in order for our action to process the `Collect` event off `lens-protocol`, we must add their `InteractionLogic` lib to our tenderly project. You can do so by clicking "Add to project" here: https://dashboard.tenderly.co/contract/mumbai/0xefd400326635e016cbfcc309725d5b62fd9d3468 + + 4. collect our post via lenster or by modifying our task `tasks/stub-collect-post.ts` diff --git a/blog/blog_gated_modules.md b/blog/blog_gated_modules.md new file mode 100644 index 0000000..1a12612 --- /dev/null +++ b/blog/blog_gated_modules.md @@ -0,0 +1,190 @@ +# Gate your Lens follows, collects, and mirrors - cross-chain - via LayerZero + +This is a technical overview of the [Lens](https://lens.xyz/) modules we developed to allow token-gated, cross-chain actions via [LayerZero](https://layerzero.network/). The source code for this entire project can be found on [this github repo](https://github.com/hyplabs/lz-collect-module). + +// @TODO: update the repo + +## Overview + +Consider the different ways that a Lens profile would want to set to gate their social content +``` +- Only wallets with 50 AAVE on Optimism can follow me +- Only owners of a CryptoPunk on Ethereum can collect my latest post +- Only collectors of my NFT collection on Arbitrum can comment on my posts +``` + +For each of these scenarios, we have a Lens module - a smart contract that processes the Lens action (follow, collect, comment/mirror) before allowing it to execute. Using LayerZero, we can enable token-gating against contracts deployed on chains other than where the Lens action is occurring (ex: `mumbai`, `polygon`). These modules are available for Lens profiles to configure with whatever parameters they want. Any supported chain, and any ERC20/ERC721 contract + token balance. + +## Cross-chain gated follows + +Let's go in depth with our `LZGatedFollowModule`, which processes our cross-chain, token-gated follows. + +In this example, a Lens profile has set their follow module to our `LZGatedFollowModule` and configured such that new followers must hold 50 AAVE tokens on Optimism. + +![lz-gated-follow-module](./lz-gated-follow-module@2x.png) + +1. **lensprotocol.lens** has set their gated follow data. A new follower must have 50 AAVE on Optimism. +2. **carlosbeltran.lens** wishes to follow - fetches the requirements and generates the signature for `#followWithSig` +3. anyone (ex: a relayer) can submit the follow requirements + the signature to the `LZGatedProxy` contract on Optimism, which relays the payload to the `LZGatedFollowModule` contract on Polygon via LayerZero + +We can see that the token balance check happens on Optimism, and the `#followWithSig` transaction is executed on Polygon. + +The intermediate contract is `LZGatedProxy`; this contract simply does the token balance check and relays the payload to our module contract on the destination chain. It's a LayerZero-enabled contract, meaning it can _send_ cross-chain payloads to trusted remote contracts. Our modules are configured to _receive_ cross-chain payloads from a trusted remote contracts. We'll explain this LayerZero wiring later. + +For now, let's look at the main functions within our follow module. + +### LZGatedFollowModule.sol +`LZGatedFollowModule` is Lens Follow Module that allows profiles to gate their following with ERC20 or ERC721 balances held on other chains. The typical flow is: +- a profile initializes this module with a supported LayerZero chain id and a erc20/erc721 contract + balance to check against +- another user wishes to follow, they read the requirements using `gatedFollowPerProfile` which points to the chain/token/balance +- user generates `followWithSigData` +- user (or relayer) submits this sig along with appropriate input to the `LZGatedProxy` contract on the other chain +- `LZGatedProxy` makes the token check, and relays the message to the `LZGatedFollowModule` on polygon +- `LZGatedFollowModule` contract validates the input, submits sig to `LensHub#followWithSig` +- follow is processed ✅ + +The two main functions in this contract are `#initializeFollowModule` and `#processFollow`; these are the callback functions the [LensHub](https://docs.lens.xyz/docs/lenshub) contract invokes when a this module is set as a profile's follow module, and when a follow action on the given profile is attempted. + +// @TODO: keep it going +// - finish this module +// - conclude that similar setup in others +// - hardhat tasks +// - finish testing +// - conclusion + +The constructor arguments include the address for the `OmniSBT` contract. The interactions with that contract are +1. validate that the `chainId` param provided in the init callback is supported +2. create a collection pointer +3. send the "mint" payload to the destination chain via layer zero + +Here's the function interface for `#initializePublicationCollectModule` + +```solidity +/** + * @dev Initialize publication collect data for the given `pubId`, including the destination chain to mint the OmniSBT + * @param profileId: the lens profile + * @param pubId: the post + * @param data: encoded data to init this module (followerOnly, chainId) + */ +function initializePublicationCollectModule( + uint256 profileId, + uint256 pubId, + bytes calldata data +) external override onlyHub returns (bytes memory) {} +``` + +When a post is initialized with this module, the caller must specify two things - whether the post can be only be collected by followers, and which LayerZero `chainId` the NFT should be minted for collectors. These values are encoded in the `data` param. + +When a post that has been initialized with this module is collected, our `#processCollect` function is invoked + +```solidity +/** + * @dev Processes a collect by: + * - [optional] ensuring the collector is a follower + * - minting a OmniSBT on the destination chain set + */ +function processCollect( + uint256, // referrerProfileId + address collector, + uint256 profileId, + uint256 pubId, + bytes calldata // data +) external override onlyHub {} +``` + +This is the function that triggers the cross-chain messaging logic in our `OmniSBT` contract. + +### OmniSBT.sol +`OmniSBT` creates Soulbound Tokens (SBTs) that mint on a remote destination chain; they are non-transferrable, and burnable. + +This contract inherits from our `LzApp` contract which wires up our contract for sending and receiving messages via LayerZero "endpoint" contracts. + +Let's look at the constructor for both contracts, as it gives us all the context we need to know before looking at the mint flow. + +```solidity +/** + * @dev OmniSBT contract constructor + * NOTE: array length will only be one when deploying to a "destination" chain. the "source" contract will contain + * all references to other deployed contracts + * @param _lzEndpoint: LayerZero endpoint on this chain to relay messages + * @param remoteChainIds: whitelisted destination chain ids (supported by LayerZero) + * @param remoteContracts: whitelisted destination contracts (deployed by us) + * @param _isSource: whether this contract is deployed on the "source" chain + */ +constructor(address _lzEndpoint, uint16[] memory remoteChainIds, bytes[] memory remoteContracts, bool _isSource) + LzApp(_lzEndpoint, msg.sender, remoteChainIds, remoteContracts) + ERC4973("Omni Soulbound Token", "OMNI-SBT") +{ + zroPaymentAddress = address(0); + isSource = _isSource; +} +``` + +```solidity +/** + * @dev LzApp contract constructor + * @param _lzEndpoint: The LZ endpoint contract deployed on this chain + * @param owner: The contract owner + * @param remoteChainIds: remote chain ids to set as trusted remotes + * @param remoteContracts: remote contracts to set as trusted remotes + */ +constructor( + address _lzEndpoint, + address owner, + uint16[] memory remoteChainIds, + bytes[] memory remoteContracts +) Owned(owner) { + if (_lzEndpoint == address(0)) { revert NotZeroAddress(); } + if (remoteChainIds.length != remoteContracts.length) { revert ArrayMismatch(); } + + lzEndpoint = ILayerZeroEndpoint(_lzEndpoint); + + uint256 length = remoteChainIds.length; + for (uint256 i = 0; i < length;) { + _lzRemoteLookup[remoteChainIds[i]] = remoteContracts[i]; + unchecked { i++; } + } +} +``` + +In short, we define the accepted chain ids and set the trusted remote contract addresses to relay messages to, and receive messages from. This way, when we process a collect for a post, we know where to relay messages based on the validated info set by the post creator. For more info on the LayerZero endpoint, check out their [docs](https://layerzero.gitbook.io/docs/faq/layerzero-endpoint). + +From here, it's helpful to picture the chain of function calls in order to understand the flow. + +![OmniSBT mint flow](./lz-collect-module@2x.png) +** it's important to note that `OmniSBT.sol` is a single contract deployed on both Polygon _and_ Optimism - it can send _and_ receive lz messages + +1. [on Polygon] someone collects our lens post +2. our callback `LZCollectModule#processCollect` is triggered +3. we call our mint function on `OmnitSBT` which makes an internal call to `#_lzSend` +4. we call `#send` on the `LayerZeroEndpoint` contract with our payload +5. LayerZero moves our payload from Polygon to Optimism via an Oracle and Relayer +6. [on Optimism] the `LayerZeroEndpoint` contract receives our payload +7. our payload is received in our `OmnitSBT` contract via the callback `#lzReceive` which makes an internal call to `#mint`, minting the NFT for the collector + +It's worth seeing how we receive messages in our `OmnitSBT` contract +```solidity +// LzApp.sol + +function lzReceive( + uint16 _srcChainId, + bytes memory _srcAddress, + uint64 _nonce, + bytes memory _payload +) public virtual override { + if (msg.sender != address(lzEndpoint)) { revert OnlyEndpoint(); } + + bytes memory trustedRemote = _lzRemoteLookup[_srcChainId]; + if (_srcAddress.length != trustedRemote.length || keccak256(_srcAddress) != keccak256(trustedRemote)) { + revert OnlyTrustedRemote(); + } + + _blockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload); +} +``` + +- we assert that only the `LayerZeroEndpoint` contract can call +- we assert that we only receive messages from trusted remote contracts (set in the constructor or via `#setTrustedRemote`) +- we pass along the arguments to `#_blockingLzReceive` to process in a **blocking** way. This means that on transaction reverts/errors `LayerZeroEndpoint` contract will block the message queue from the "source" chain until the transaction is retried successfully ([see more](https://layerzero.gitbook.io/docs/faq/messaging-properties#message-ordering)) + +The end result: the account that collected the post on Polygon now has a soulbound NFT on Optimism :partying_face: diff --git a/blog/lz-gated-follow-module@2x.png b/blog/lz-gated-follow-module@2x.png new file mode 100644 index 0000000..7354a4f Binary files /dev/null and b/blog/lz-gated-follow-module@2x.png differ diff --git a/contracts/LZGatedCollectModule.sol b/contracts/LZGatedCollectModule.sol new file mode 100644 index 0000000..2e63cf1 --- /dev/null +++ b/contracts/LZGatedCollectModule.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import {ModuleBase, Errors} from "@aave/lens-protocol/contracts/core/modules/ModuleBase.sol"; +import {ICollectModule} from '@aave/lens-protocol/contracts/interfaces/ICollectModule.sol'; +import {FollowValidationModuleBase} from '@aave/lens-protocol/contracts/core/modules/FollowValidationModuleBase.sol'; +import {ILensHub} from "@aave/lens-protocol/contracts/interfaces/ILensHub.sol"; +import {DataTypes} from "@aave/lens-protocol/contracts/libraries/DataTypes.sol"; +import {LzApp} from "./lz/LzApp.sol"; + +/** + * @title LZGatedCollectModule + * + * @notice A Lens Collect Module that allows profiles to gate who can collect their post with ERC20 or ERC721 balances + * held on other chains. + */ +contract LZGatedCollectModule is FollowValidationModuleBase, ICollectModule, LzApp { + struct GatedCollectData { + address tokenContract; // the remote contract to read from + uint256 balanceThreshold; // result of balanceOf() should be greater than or equal to + uint16 remoteChainId; // the remote chainId to read against + } + + event InitCollectModule( + uint256 indexed profileId, + uint256 indexed pubId, + address tokenContract, + uint256 balanceThreshold, + uint16 chainId + ); + + mapping (uint256 => mapping (uint256 => GatedCollectData)) public gatedCollectDataPerPub; // profileId => pubId => gated collect data + mapping (uint256 => mapping (uint256 => mapping (address => bool))) public validatedCollectors; // profileIdPointed => pubId => profiles which have been validated + + /** + * @dev contract constructor + * @param hub LensHub + * @param _lzEndpoint: LayerZero endpoint on this chain to relay messages + * @param remoteChainIds: whitelisted destination chain ids (supported by LayerZero) + * @param remoteProxies: proxy destination contracts (deployed by us) + */ + constructor( + address hub, + address _lzEndpoint, + uint16[] memory remoteChainIds, + bytes[] memory remoteProxies + ) ModuleBase(hub) LzApp(_lzEndpoint, msg.sender, remoteChainIds, remoteProxies) {} + + /** + * @notice Initialize this collect module for the given profile/publication + * + * @param profileId The profile ID of the profile creating the pub + * @param pubId The pub to init this reference module to + * @param data The arbitrary data parameter, which in this particular module initialization will be just ignored. + * + * @return bytes Empty bytes. + */ + function initializePublicationCollectModule( + uint256 profileId, + uint256 pubId, + bytes calldata data + ) external override onlyHub returns (bytes memory) { + ( + address tokenContract, + uint256 balanceThreshold, + uint16 chainId + ) = abi.decode(data, (address, uint256, uint16)); + + if (address(tokenContract) == address(0) || _lzRemoteLookup[chainId].length == 0) { + revert Errors.InitParamsInvalid(); + } + + // anyone can read this data before attempting to follow the given profile + gatedCollectDataPerPub[profileId][pubId] = GatedCollectData({ + remoteChainId: chainId, + tokenContract: tokenContract, + balanceThreshold: balanceThreshold + }); + + emit InitCollectModule(profileId, pubId, tokenContract, balanceThreshold, chainId); + + return new bytes(0); + } + + /** + * @dev Process a collect by: + * - checking that we have already validated the collector through our `LZGatedProxy` on a remote chain + */ + function processCollect( + uint256, // referrerProfileId + address collector, + uint256 profileId, + uint256 pubId, + bytes calldata // data + ) external view override onlyHub { + if (!validatedCollectors[profileId][pubId][collector]) { + revert Errors.CollectNotAllowed(); + } + } + + /** + * @dev Callback from our `LZGatedProxy` contract deployed on a remote chain, signals that the collect is validated + * NOTE: this function is actually non-blocking in that it does not explicitly revert and catches external errors + */ + function _blockingLzReceive( + uint16 _srcChainId, + bytes memory _srcAddress, + uint64 _nonce, + bytes memory _payload + ) internal override { + ( + address token, + uint256 threshold, + DataTypes.CollectWithSigData memory collectSig + ) = abi.decode(_payload, (address, uint256, DataTypes.CollectWithSigData)); + + GatedCollectData memory data = gatedCollectDataPerPub[collectSig.profileId][collectSig.pubId]; + + // validate that remote check was against the contract/threshold defined + if (data.remoteChainId != _srcChainId || data.balanceThreshold != threshold || data.tokenContract != token) { + emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, 'InvalidRemoteInput'); + return; + } + + // @TODO: hash the vars vs deeply nested? + validatedCollectors[collectSig.profileId][collectSig.pubId][collectSig.collector] = true; + + // use the signature to execute the collect + try ILensHub(HUB).collectWithSig(collectSig) {} + catch Error (string memory reason) { + emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, reason); + } + + delete validatedCollectors[collectSig.profileId][collectSig.pubId][collectSig.collector]; + } +} diff --git a/contracts/LZGatedFollowModule.sol b/contracts/LZGatedFollowModule.sol new file mode 100644 index 0000000..3529ec5 --- /dev/null +++ b/contracts/LZGatedFollowModule.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import {ModuleBase, Errors} from "@aave/lens-protocol/contracts/core/modules/ModuleBase.sol"; +import { + FollowValidatorFollowModuleBase +} from "@aave/lens-protocol/contracts/core/modules/follow/FollowValidatorFollowModuleBase.sol"; +import {ILensHub} from "@aave/lens-protocol/contracts/interfaces/ILensHub.sol"; +import {DataTypes} from "@aave/lens-protocol/contracts/libraries/DataTypes.sol"; +import {LzApp} from "./lz/LzApp.sol"; + +/** + * @title LZGatedFollowModule + * + * @notice A Lens Follow Module that allows profiles to gate their following with ERC20 or ERC721 balances held + * on other chains. + */ +contract LZGatedFollowModule is FollowValidatorFollowModuleBase, LzApp { + struct GatedFollowData { + address tokenContract; // the remote contract to read from + uint256 balanceThreshold; // result of balanceOf() should be greater than or equal to + uint16 remoteChainId; // the remote chainId to read against + } + + event InitFollowModule(uint256 indexed profileId, address tokenContract, uint256 balanceThreshold, uint16 chainId); + + mapping (uint256 => GatedFollowData) public gatedFollowPerProfile; // profileId => gated follow data + mapping (uint256 => mapping (address => bool)) public validatedFollowers; // profileId => address which has been validated + + /** + * @dev contract constructor + * @param hub LensHub + * @param _lzEndpoint: LayerZero endpoint on this chain to relay messages + * @param remoteChainIds: whitelisted destination chain ids (supported by LayerZero) + * @param remoteProxies: proxy destination contracts (deployed by us) + */ + constructor( + address hub, + address _lzEndpoint, + uint16[] memory remoteChainIds, + bytes[] memory remoteProxies + ) ModuleBase(hub) LzApp(_lzEndpoint, msg.sender, remoteChainIds, remoteProxies) {} + + /** + * @notice Initialize this follow module for the given profile + * + * @param profileId The profile ID of the profile to initialize this module for. + * @param data The arbitrary data parameter, which in this particular module initialization will be just ignored. + * + * @return bytes Empty bytes. + */ + function initializeFollowModule(uint256 profileId, bytes calldata data) + external + override + onlyHub + returns (bytes memory) + { + ( + address tokenContract, + uint256 balanceThreshold, + uint16 chainId + ) = abi.decode(data, (address, uint256, uint16)); + + if (address(tokenContract) == address(0) || _lzRemoteLookup[chainId].length == 0) { + revert Errors.InitParamsInvalid(); + } + + // anyone can read this data before attempting to follow the given profile + gatedFollowPerProfile[profileId] = GatedFollowData({ + remoteChainId: chainId, + tokenContract: tokenContract, + balanceThreshold: balanceThreshold + }); + + emit InitFollowModule(profileId, tokenContract, balanceThreshold, chainId); + + return new bytes(0); + } + + /** + * @dev Process a follow by: + * - checking that we have already validated the follower through our `LZGatedProxy` on a remote chain + */ + function processFollow( + address follower, + uint256 profileId, + bytes calldata // data + ) external view override onlyHub { + if (!validatedFollowers[profileId][follower]) { + revert Errors.FollowInvalid(); + } + } + + /** + * @dev We don't need to execute any additional logic on transfers in this follow module. + */ + function followModuleTransferHook( + uint256 profileId, + address from, + address to, + uint256 followNFTTokenId + ) external override {} + + /** + * @dev Callback from our `LZGatedProxy` contract deployed on a remote chain, signals that the follow is validated + * NOTE: this function is actually non-blocking in that it does not explicitly revert and catches external errors + */ + function _blockingLzReceive( + uint16 _srcChainId, + bytes memory _srcAddress, + uint64 _nonce, + bytes memory _payload + ) internal override { + ( + address token, + uint256 threshold, + DataTypes.FollowWithSigData memory followSig + ) = abi.decode(_payload, (address, uint256, DataTypes.FollowWithSigData)); + + uint256 profileId = followSig.profileIds[0]; + GatedFollowData memory data = gatedFollowPerProfile[profileId]; + + // validate that remote check was against the contract/threshold defined + if (data.remoteChainId != _srcChainId || data.balanceThreshold != threshold || data.tokenContract != token) { + emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, 'InvalidRemoteInput'); + return; + } + + // allow the follow in the callback to #processFollow + validatedFollowers[profileId][followSig.follower] = true; + + // use the signature to execute the follow + try ILensHub(HUB).followWithSig(followSig) {} + catch Error (string memory reason) { + emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, reason); + } + + delete validatedFollowers[profileId][followSig.follower]; + } +} diff --git a/contracts/LZGatedProxy.sol b/contracts/LZGatedProxy.sol new file mode 100644 index 0000000..9f4a52b --- /dev/null +++ b/contracts/LZGatedProxy.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import {DataTypes} from "@aave/lens-protocol/contracts/libraries/DataTypes.sol"; +import "./lz/SimpleLzApp.sol"; + +/** + * @title LZGatedProxy + * @notice This contract acts as a proxy for our `LZGated*` Lens modules in order to read + * token balances from remote contracts on any chain supported by LayerZero. + */ +contract LZGatedProxy is SimpleLzApp { + error InsufficientBalance(); + error NotAccepting(); + + bytes public remoteFollowModule; // LZGatedFollowModule + bytes public remoteReferenceModule; // LZGatedReferenceModule + bytes public remoteCollectModule; // LZGatedCollectModule + + /** + * @dev contract constructor + * @param _lzEndpoint: The lz endpoint contract deployed on this chain + * @param _remoteChainId: remote chain id to be set as the trusted remote + * @param _remoteFollowModule: trusted follow module on the remote chain + * @param _remoteReferenceModule: trusted reference module on the remote chain + * @param _remoteCollectModule: trusted collect module on the remote chain + */ + constructor( + address _lzEndpoint, + uint16 _remoteChainId, + bytes memory _remoteFollowModule, + bytes memory _remoteReferenceModule, + bytes memory _remoteCollectModule + ) SimpleLzApp(_lzEndpoint, msg.sender, _remoteChainId) { + remoteFollowModule = _remoteFollowModule; + remoteReferenceModule = _remoteReferenceModule; + remoteCollectModule = _remoteCollectModule; + } + + /** + * @notice validate a token balance on this chain before relaying the intent to follow a Lens profile on the remote + * chain. + * NOTE: callers of this function MUST pass the exact values for `tokenContract` and `balanceThreshold` returned from + * the call to LZGatedFollowModule.gatedFollowPerProfile(profileId) - or the transaction on the remote chain WILL + * revert. + * @param tokenContract: the ERC20/ERC721 contract set by the `profileId` to check a balance against + * @param balanceThreshold: the amount of tokens required in order for a successful follow + * @param lzCustomGasAmount: custom gas amount that is paid for lz.send() + * @param followSig: the follow signature expected by the LensHub + */ + function relayFollowWithSig( + address tokenContract, + uint256 balanceThreshold, + uint256 lzCustomGasAmount, + DataTypes.FollowWithSigData memory followSig + ) external payable { + if (!_checkThreshold(followSig.follower, tokenContract, balanceThreshold)) { revert InsufficientBalance(); } + + _lzSend( + remoteFollowModule, + abi.encode( + tokenContract, + balanceThreshold, + followSig + ), + payable(msg.sender), + _getAdapterParams(lzCustomGasAmount) + ); + } + + /** + * TODO: validate that `sender` is the one who signed `commentSig` (ecrecover) + * @notice validate a token balance on this chain before relaying the intent to comment on a Lens post on the remote + * chain. + * NOTE: callers of this function MUST pass the exact values for `tokenContract` and `balanceThreshold` returned from + * the call to LZGatedReferenceModule.gatedReferenceDataPerPub(profileIdPointed, pubIdPointed) - or the transaction + * on the remote chain WILL revert. + * @param sender: the account wishing to perform the comment action + * @param tokenContract: the ERC20/ERC721 contract set by the `profileId` to check a balance against + * @param balanceThreshold: the amount of tokens required in order for a successful follow + * @param lzCustomGasAmount: custom gas amount that is paid for lz.send() + * @param commentSig: the comment signature expected by the LensHub + */ + function relayCommentWithSig( + address sender, + address tokenContract, + uint256 balanceThreshold, + uint256 lzCustomGasAmount, + DataTypes.CommentWithSigData memory commentSig + ) external payable { + if (!_checkThreshold(sender, tokenContract, balanceThreshold)) { revert InsufficientBalance(); } + + _lzSend( + remoteReferenceModule, + abi.encode( + true, // isComment + tokenContract, + balanceThreshold, + commentSig + ), + payable(msg.sender), + _getAdapterParams(lzCustomGasAmount) + ); + } + + /** + * TODO: validate that `sender` is the one who signed `mirrorSig` (ecrecover) + * @notice validate a token balance on this chain before relaying the intent to mirror a Lens post on the remote + * chain. + * NOTE: callers of this function MUST pass the exact values for `tokenContract` and `balanceThreshold` returned from + * the call to LZGatedReferenceModule.gatedReferenceDataPerPub(profileIdPointed, pubIdPointed) - or the transaction + * on the remote chain WILL revert. + * @param sender: the account wishing to perform the mirror action + * @param tokenContract: the ERC20/ERC721 contract set by the `profileId` to check a balance against + * @param balanceThreshold: the amount of tokens required in order for a successful follow + * @param lzCustomGasAmount: custom gas amount that is paid for lz.send() + * @param mirrorSig: the mirror signature expected by the LensHub + */ + function relayMirrorWithSig( + address sender, + address tokenContract, + uint256 balanceThreshold, + uint256 lzCustomGasAmount, + DataTypes.MirrorWithSigData memory mirrorSig + ) external payable { + if (!_checkThreshold(sender, tokenContract, balanceThreshold)) { revert InsufficientBalance(); } + + _lzSend( + remoteReferenceModule, + abi.encode( + false, // isComment + tokenContract, + balanceThreshold, + mirrorSig + ), + payable(msg.sender), + _getAdapterParams(lzCustomGasAmount) + ); + } + + /** + * @notice validate a token balance on this chain before relaying the intent to collect a Lens post on the remote + * chain. + * NOTE: callers of this function MUST pass the exact values for `tokenContract` and `balanceThreshold` returned from + * the call to LZGatedCollectModule.gatedCollectDataPerPub(profileId, pubId) - or the transaction + * on the remote chain WILL revert. + * @param tokenContract: the ERC20/ERC721 contract set by the `profileId` to check a balance against + * @param balanceThreshold: the amount of tokens required in order for a successful follow + * @param lzCustomGasAmount: custom gas amount that is paid for lz.send() + * @param collectSig: the collect signature expected by the LensHub + */ + function relayCollectWithSig( + address tokenContract, + uint256 balanceThreshold, + uint256 lzCustomGasAmount, + DataTypes.CollectWithSigData memory collectSig + ) external payable { + if (!_checkThreshold(collectSig.collector, tokenContract, balanceThreshold)) { revert InsufficientBalance(); } + + _lzSend( + remoteCollectModule, + abi.encode( + tokenContract, + balanceThreshold, + collectSig + ), + payable(msg.sender), + _getAdapterParams(lzCustomGasAmount) + ); + } + + /** + * @dev not accepting native tokens + */ + receive() external payable { revert NotAccepting(); } + + /** + * @dev Check that `account` meets the `balanceThreshold` of held tokens in `tokenContract`; we use the standard + * `#balanceOf` function signature for ERC721 and ERC20, and simply return false on any error thrown. + */ + function _checkThreshold(address account, address tokenContract, uint256 balanceThreshold) private returns (bool) { + ( + bool success, + bytes memory result + ) = tokenContract.call(abi.encodeWithSignature("balanceOf(address)", account)); + + if (!success) return false; + + (uint256 balance) = abi.decode(result, (uint256)); + + return balance >= balanceThreshold; + } + + /** + * @dev returns the adapter params (version 1) required to override the gas provided for the tx on the destination + * chain: https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters + */ + function _getAdapterParams(uint256 gasAmount) private pure returns (bytes memory adapterParams) { + adapterParams = gasAmount > 0 + ? abi.encodePacked(uint16(1), gasAmount) + : bytes(""); + } +} diff --git a/contracts/LZGatedReferenceModule.sol b/contracts/LZGatedReferenceModule.sol new file mode 100644 index 0000000..d9714cd --- /dev/null +++ b/contracts/LZGatedReferenceModule.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import {IReferenceModule} from '@aave/lens-protocol/contracts/interfaces/IReferenceModule.sol'; +import {ModuleBase, Errors} from "@aave/lens-protocol/contracts/core/modules/ModuleBase.sol"; +import {FollowValidationModuleBase} from '@aave/lens-protocol/contracts/core/modules/FollowValidationModuleBase.sol'; +import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol'; +import {ILensHub} from "@aave/lens-protocol/contracts/interfaces/ILensHub.sol"; +import {DataTypes} from "@aave/lens-protocol/contracts/libraries/DataTypes.sol"; +import {LzApp} from "./lz/LzApp.sol"; +import "hardhat/console.sol"; + +/** + * @title LZGatedReferenceModule + * + * @notice A Lens Reference Module that allows publication creators to gate who can comment/mirror their post with + * ERC20 or ERC721 balances held on other chains. + */ +contract LZGatedReferenceModule is FollowValidationModuleBase, IReferenceModule, LzApp { + struct GatedReferenceData { + address tokenContract; // the remote contract to read from + uint256 balanceThreshold; // result of balanceOf() should be greater than or equal to + uint16 remoteChainId; // the remote chainId to read against + } + + event InitReferenceModule(uint256 indexed profileId, uint256 indexed pubId, address tokenContract, uint256 balanceThreshold, uint16 chainId); + + error CommentOrMirrorInvalid(); + + mapping (uint256 => mapping (uint256 => GatedReferenceData)) public gatedReferenceDataPerPub; // profileId => pubId => gated reference data + mapping (uint256 => mapping (uint256 => mapping (uint256 => bool))) public validatedReferencers; // profileIdPointed => pubId => profiles which have been validated + + /** + * @dev contract constructor + * @param hub LensHub + * @param _lzEndpoint: LayerZero endpoint on this chain to relay messages + * @param remoteChainIds: whitelisted destination chain ids (supported by LayerZero) + * @param remoteProxies: proxy destination contracts (deployed by us) + */ + constructor( + address hub, + address _lzEndpoint, + uint16[] memory remoteChainIds, + bytes[] memory remoteProxies + ) ModuleBase(hub) LzApp(_lzEndpoint, msg.sender, remoteChainIds, remoteProxies) {} + + /** + * @notice Initialize this reference module for the given profile/publication + * + * @param profileId The profile ID of the profile creating the pub + * @param pubId The pub to init this reference module to + * @param data The arbitrary data parameter, which in this particular module initialization will be just ignored. + * + * @return bytes Empty bytes. + */ + function initializeReferenceModule(uint256 profileId, uint256 pubId, bytes calldata data) + external + override + onlyHub + returns (bytes memory) + { + ( + address tokenContract, + uint256 balanceThreshold, + uint16 chainId + ) = abi.decode(data, (address, uint256, uint16)); + + if (address(tokenContract) == address(0) || _lzRemoteLookup[chainId].length == 0) { + revert Errors.InitParamsInvalid(); + } + + // anyone can read this data before attempting to follow the given profile + gatedReferenceDataPerPub[profileId][pubId] = GatedReferenceData({ + remoteChainId: chainId, + tokenContract: tokenContract, + balanceThreshold: balanceThreshold + }); + + emit InitReferenceModule(profileId, pubId, tokenContract, balanceThreshold, chainId); + + return new bytes(0); + } + + /** + * @dev Process a comment by: + * - checking that we have already validated the commentor through our `LZGatedProxy` on a remote chain + */ + function processComment( + uint256 profileId, + uint256 profileIdPointed, + uint256 pubIdPointed, + bytes calldata // data + ) external view override onlyHub { + if (!validatedReferencers[profileIdPointed][pubIdPointed][profileId]) { + revert CommentOrMirrorInvalid(); + } + } + + /** + * @dev Process a mirror by: + * - checking that we have already validated the mirrorer through our `LZGatedProxy` on a remote chain + */ + function processMirror( + uint256 profileId, + uint256 profileIdPointed, + uint256 pubIdPointed, + bytes calldata // data + ) external view override onlyHub { + if (!validatedReferencers[profileIdPointed][pubIdPointed][profileId]) { + revert CommentOrMirrorInvalid(); + } + } + + /** + * @dev Callback from our `LZGatedProxy` contract deployed on a remote chain, signals that the comment/mirror + * is validated + * NOTE: this function is actually non-blocking in that it does not explicitly revert and catches external errors + */ + function _blockingLzReceive( + uint16 _srcChainId, + bytes memory _srcAddress, + uint64 _nonce, + bytes memory _payload + ) internal override { + (bool isComment,,,) = abi.decode(_payload, (bool, address, uint256, bytes)); + + // parse the payload for either #commentWithSig or #mirrorWithSig + string memory error = isComment ? _handleComment(_srcChainId, _payload) : _handleMirror(_srcChainId, _payload); + + if (bytes(error).length > 0) { + emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, error); + } + } + + /** + * @dev Decodes the `payload` for Lens#commentWithSig + * @return error an error string if the call failed, else empty string + */ + function _handleComment(uint16 _srcChainId, bytes memory _payload) internal returns (string memory error) { + (,address token, uint256 threshold, DataTypes.CommentWithSigData memory commentSig) = abi.decode( + _payload, + (bool, address, uint256, DataTypes.CommentWithSigData) + ); + + GatedReferenceData memory data = gatedReferenceDataPerPub[commentSig.profileIdPointed][commentSig.pubIdPointed]; + + // validate that remote check was against the contract/threshold defined + if (data.remoteChainId != _srcChainId || data.balanceThreshold != threshold || data.tokenContract != token) { + return error = "InvalidRemoteInput"; + } + + // @TODO: hash the vars vs deeply nested? + validatedReferencers[commentSig.profileIdPointed][commentSig.pubIdPointed][commentSig.profileId] = true; + + try ILensHub(HUB).commentWithSig(commentSig) { + error = ""; + } catch Error (string memory reason) { + error = reason; + } + + delete validatedReferencers[commentSig.profileIdPointed][commentSig.pubIdPointed][commentSig.profileId]; + } + + /** + * @dev Decodes the `payload` for Lens#mirrorWithSig + * @return error an error string if the call failed, else empty string + */ + function _handleMirror(uint16 _srcChainId, bytes memory _payload) internal returns (string memory error) { + (,address token, uint256 threshold, DataTypes.MirrorWithSigData memory mirrorSig) = abi.decode( + _payload, + (bool, address, uint256, DataTypes.MirrorWithSigData) + ); + + GatedReferenceData memory data = gatedReferenceDataPerPub[mirrorSig.profileIdPointed][mirrorSig.pubIdPointed]; + + // validate that remote check was against the contract/threshold defined + if (data.remoteChainId != _srcChainId || data.balanceThreshold != threshold || data.tokenContract != token) { + return error = "InvalidRemoteInput"; + } + + // @TODO: hash the vars vs deeply nested? + validatedReferencers[mirrorSig.profileIdPointed][mirrorSig.pubIdPointed][mirrorSig.profileId] = true; + + try ILensHub(HUB).mirrorWithSig(mirrorSig) { + error = ""; + } catch Error (string memory reason) { + error = reason; + } + + delete validatedReferencers[mirrorSig.profileIdPointed][mirrorSig.pubIdPointed][mirrorSig.profileId]; + } +} diff --git a/contracts/OmniSBT.sol b/contracts/OmniSBT.sol index 44b5ec9..178d0af 100644 --- a/contracts/OmniSBT.sol +++ b/contracts/OmniSBT.sol @@ -17,7 +17,6 @@ contract OmniSBT is IOmniSBT, URIStorage, ERC4973, LzApp { error OnlyTokenOwner(); address public collectModule; // the FollowCampaignModule contract that can create collections and mint - address public zroPaymentAddress; // ZRO payment address uint256 public collections; // counter for collections; 1-based mapping (uint256 => uint256) public tokenToCollectionId; // _tokenIdCounter => collections @@ -45,7 +44,6 @@ contract OmniSBT is IOmniSBT, URIStorage, ERC4973, LzApp { LzApp(_lzEndpoint, msg.sender, remoteChainIds, remoteContracts) ERC4973("Omni Soulbound Token", "OMNI-SBT") { - zroPaymentAddress = address(0); isSource = _isSource; } @@ -86,7 +84,6 @@ contract OmniSBT is IOmniSBT, URIStorage, ERC4973, LzApp { chainId, abi.encode(collector, collectionId, _tokenIdCounter, URIStorage.tokenURI(collectionId)), payable(collector), - zroPaymentAddress, bytes("") ); @@ -130,14 +127,6 @@ contract OmniSBT is IOmniSBT, URIStorage, ERC4973, LzApp { collectModule = _collectModule; } - /** - * @notice allows the contract owner to set the ZRO payment address - * @param _zroPaymentAddress: ZRO token address used as alternative payment for relayed messages - */ - function setZroPaymentAddress(address _zroPaymentAddress) external onlyOwner { - zroPaymentAddress = _zroPaymentAddress; - } - /** * @notice EIP-165 */ diff --git a/contracts/lz/LzApp.sol b/contracts/lz/LzApp.sol index 61ec264..260c6b2 100644 --- a/contracts/lz/LzApp.sol +++ b/contracts/lz/LzApp.sol @@ -8,7 +8,8 @@ import "./interfaces/ILayerZeroUserApplicationConfig.sol"; import "./interfaces/ILayerZeroEndpoint.sol"; /** - * + * @title LzApp + * @notice LayerZero-enabled contract that can have multiple remote chain ids. */ abstract contract LzApp is Owned, ILayerZeroReceiver, ILayerZeroUserApplicationConfig { error NotZeroAddress(); @@ -16,9 +17,14 @@ abstract contract LzApp is Owned, ILayerZeroReceiver, ILayerZeroUserApplicationC error OnlyEndpoint(); error RemoteNotFound(); error OnlyTrustedRemote(); + error NotAccepting(); + + event MessageFailed(uint16 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload, string _reason); ILayerZeroEndpoint public immutable lzEndpoint; + address public zroPaymentAddress; // the address of the ZRO token holder who would pay for the transaction + mapping (uint16 => bytes) internal _lzRemoteLookup; // chainId (lz) => endpoint /** @@ -46,49 +52,38 @@ abstract contract LzApp is Owned, ILayerZeroReceiver, ILayerZeroUserApplicationC } } - function _lzSend( - uint16 _dstChainId, - bytes memory _payload, - address payable _refundAddress, - address _zroPaymentAddress, - bytes memory _adapterParams - ) internal virtual { - if (_lzRemoteLookup[_dstChainId].length == 0) { revert RemoteNotFound(); } - - lzEndpoint.send{value: msg.value}( - _dstChainId, - _lzRemoteLookup[_dstChainId], - _payload, - _refundAddress, - _zroPaymentAddress, - _adapterParams - ); - } + /** + * @dev not accepting native tokens + */ + receive() external virtual payable { revert NotAccepting(); } + /** + * @dev receives a cross-chain message from the lz endpoint contract deployed on this chain + * NOTE: this is non-blocking in the sense that it does not explicitly revert, but of course does not catch all + * potential errors thrown. + * @param _srcChainId: the remote chain id + * @param _srcAddress: the remote contract sending the message + * @param _nonce: the message nonce + * @param _payload: the message payload + */ function lzReceive( uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload ) public virtual override { - if (msg.sender != address(lzEndpoint)) { revert OnlyEndpoint(); } + if (msg.sender != address(lzEndpoint)) { + emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, 'OnlyEndpoint'); + } bytes memory trustedRemote = _lzRemoteLookup[_srcChainId]; if (_srcAddress.length != trustedRemote.length || keccak256(_srcAddress) != keccak256(trustedRemote)) { - revert OnlyTrustedRemote(); + emit MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, 'OnlyTrustedRemote'); } _blockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload); } - // @dev to be overriden by the concrete class - function _blockingLzReceive( - uint16 _srcChainId, - bytes memory _srcAddress, - uint64 _nonce, - bytes memory _payload - ) internal virtual; - function setTrustedRemote(uint16 _srcChainId, bytes calldata _srcAddress) external onlyOwner { _lzRemoteLookup[_srcChainId] = _srcAddress; } @@ -111,6 +106,10 @@ abstract contract LzApp is Owned, ILayerZeroReceiver, ILayerZeroUserApplicationC lzEndpoint.setReceiveVersion(_version); } + function setZroPaymentAddress(address _zroPaymentAddress) external onlyOwner { + zroPaymentAddress = _zroPaymentAddress; + } + function forceResumeReceive(uint16 _srcChainId, bytes calldata _srcAddress) external override onlyOwner { lzEndpoint.forceResumeReceive(_srcChainId, _srcAddress); } @@ -118,4 +117,30 @@ abstract contract LzApp is Owned, ILayerZeroReceiver, ILayerZeroUserApplicationC function getConfig(uint16 _version, uint16 _chainId, address, uint _configType) external view returns (bytes memory) { return lzEndpoint.getConfig(_version, _chainId, address(this), _configType); } + + function _lzSend( + uint16 _dstChainId, + bytes memory _payload, + address payable _refundAddress, + bytes memory _adapterParams + ) internal virtual { + if (_lzRemoteLookup[_dstChainId].length == 0) { revert RemoteNotFound(); } + + lzEndpoint.send{value: msg.value}( + _dstChainId, + _lzRemoteLookup[_dstChainId], + _payload, + _refundAddress, + zroPaymentAddress, + _adapterParams + ); + } + + // @dev to be overriden by the concrete class + function _blockingLzReceive( + uint16 _srcChainId, + bytes memory _srcAddress, + uint64 _nonce, + bytes memory _payload + ) internal virtual; } diff --git a/contracts/lz/SimpleLzApp.sol b/contracts/lz/SimpleLzApp.sol new file mode 100644 index 0000000..f5bc486 --- /dev/null +++ b/contracts/lz/SimpleLzApp.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import "@rari-capital/solmate/src/auth/Owned.sol"; +import "./interfaces/ILayerZeroUserApplicationConfig.sol"; +import "./interfaces/ILayerZeroEndpoint.sol"; + +/** + * @title SimpleLzApp + * @notice LayerZero-enabled contract that has only one trusted remote chain and sends messages cross-chain (does NOT + * receive) to remote contracts provided by the concrete class. + * NOTE: this contract is ownable only to set layerzero configs. + */ +abstract contract SimpleLzApp is Owned, ILayerZeroUserApplicationConfig { + error NotZeroAddress(); + + ILayerZeroEndpoint public immutable lzEndpoint; // lz endpoint contract deployed on this chain + + uint16 public remoteChainId; // lz chain id + address public zroPaymentAddress; // the address of the ZRO token holder who would pay for all transactions + + /** + * @dev contract constructor + * @param _lzEndpoint: the lz endpoint contract deployed on this chain + * @param _owner: the Contract owner + * @param _remoteChainId: remote chain id to be set as the trusted remote + */ + constructor(address _lzEndpoint, address _owner, uint16 _remoteChainId) Owned(_owner) { + if (_lzEndpoint == address(0)) { revert NotZeroAddress(); } + + lzEndpoint = ILayerZeroEndpoint(_lzEndpoint); + remoteChainId = _remoteChainId; + } + + /** + * @notice generic config for LayerZero user application + */ + function setConfig( + uint16 _version, + uint16 _chainId, + uint _configType, + bytes calldata _config + ) external override onlyOwner { + lzEndpoint.setConfig(_version, _chainId, _configType, _config); + } + + /** + * @notice allows the contract owner to set the lz config for send version + */ + function setSendVersion(uint16 _version) external override onlyOwner { + lzEndpoint.setSendVersion(_version); + } + + /** + * @notice allows the contract owner to set the lz config for receive version + */ + function setReceiveVersion(uint16 _version) external override onlyOwner { + lzEndpoint.setReceiveVersion(_version); + } + + /** + * @notice allows the contract owner to set the `_zroPaymentAddress` responsible for paying all transactions in ZRO + */ + function setZroPaymentAddress(address _zroPaymentAddress) external onlyOwner { + zroPaymentAddress = _zroPaymentAddress; + } + + /** + * @notice allows the contract owner to unblock the queue of messages + */ + function forceResumeReceive(uint16 _srcChainId, bytes calldata _srcAddress) external override onlyOwner { + lzEndpoint.forceResumeReceive(_srcChainId, _srcAddress); + } + + /** + * @notice returns the lz config for this contract + */ + function getConfig(uint16 _version, uint16 _chainId, address, uint _configType) external view returns (bytes memory) { + return lzEndpoint.getConfig(_version, _chainId, address(this), _configType); + } + + /** + * @dev sends a cross-chain message to the lz endpoint contract deployed on this chain, to be relayed + * @param _remoteContract: the trusted contract on the remote chain to receive the message + * @param _payload: the actual message to be relayed + * @param _refundAddress: the address on this chain to receive the refund - excess paid for gas + * @param _adapterParams: the custom adapter params to use in sending this message + */ + function _lzSend( + bytes storage _remoteContract, + bytes memory _payload, + address payable _refundAddress, + bytes memory _adapterParams + ) internal virtual { + // remote address concated with local address packed into 40 bytes + bytes memory remoteAndLocalAddresses = abi.encodePacked(_remoteContract, address(this)); + + lzEndpoint.send{value: msg.value}( + remoteChainId, + remoteAndLocalAddresses, + _payload, + _refundAddress, + zroPaymentAddress, + _adapterParams + ); + } +} diff --git a/contracts/mocks/ERC20Mock.sol b/contracts/mocks/ERC20Mock.sol new file mode 100644 index 0000000..751b699 --- /dev/null +++ b/contracts/mocks/ERC20Mock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract ERC20Mock is ERC20, Ownable { + constructor() ERC20("ERC20Mock", "MOCK") {} + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/contracts/mocks/ERC721Mock.sol b/contracts/mocks/ERC721Mock.sol new file mode 100644 index 0000000..f268dd4 --- /dev/null +++ b/contracts/mocks/ERC721Mock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +contract ERC721Mock is ERC721, Ownable { + using Counters for Counters.Counter; + + Counters.Counter private _tokenIdCounter; + + constructor() ERC721("ERC721Mock", "MOCK") {} + + function safeMint(address to) public onlyOwner { + uint256 tokenId = _tokenIdCounter.current(); + _tokenIdCounter.increment(); + _safeMint(to, tokenId); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index e9c0565..ddc6de3 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -95,11 +95,15 @@ const config = { chainId: 97, accounts: [process.env.DEPLOYER_PRIVATE_KEY] }, - // // not supported by LayerZero yet - // optimism_goerli: { - // url: process.env.ALCHEMY_OPTIMISM_GOERLI_URL, - // accounts: [process.env.DEPLOYER_PRIVATE_KEY] - // }, + goerli: { + url: process.env.ALCHEMY_GOERLI_URL, + accounts: [process.env.DEPLOYER_PRIVATE_KEY, ...lensTestWallets()] + }, + optimismTestnet: { + url: process.env.ALCHEMY_OPTIMISM_GOERLI_URL, + accounts: [process.env.DEPLOYER_PRIVATE_KEY, ...lensTestWallets()], + chainId: 420, + }, docker: { url: 'http://127.0.0.1:8545', accounts: lensTestWallets() diff --git a/package.json b/package.json index 4dc109c..1dcae51 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "@aave/lens-protocol": "^1.0.2", "@openzeppelin/contracts": "^4.3.2", "@rari-capital/solmate": "^6.4.0", - "@tenderly/hardhat-tenderly": "^1.1.5" + "@tenderly/hardhat-tenderly": "^1.1.5", + "promise-limit": "^2.7.0" }, "devDependencies": { "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", diff --git a/scripts/utils/goerli-contracts.json b/scripts/utils/goerli-contracts.json new file mode 100644 index 0000000..549141f --- /dev/null +++ b/scripts/utils/goerli-contracts.json @@ -0,0 +1,3 @@ +{ + "LZGatedProxy": "0x2cBF9A2F164872cB4FF1424F7729847ffc58F3Fb" +} \ No newline at end of file diff --git a/scripts/utils/mumbai-contracts.json b/scripts/utils/mumbai-contracts.json index 68c8fc4..edec06b 100644 --- a/scripts/utils/mumbai-contracts.json +++ b/scripts/utils/mumbai-contracts.json @@ -1,3 +1,6 @@ { - "OmniSBT": "0x1a255753a44EEedbf6d3Afe65A8F006F8C513b0C" + "OmniSBT": "0x1a255753a44EEedbf6d3Afe65A8F006F8C513b0C", + "LZGatedFollowModule": "0x68D0aA7D24926e5665e92E9A2aED8de772b64385", + "LZGatedReferenceModule": "0x46720e2AC58f69d20A952F51AE73cFE0ae4a73b7", + "LZGatedCollectModule": "0x2f4A8D959fD7227d0942D220d3E8FDCECd757dC8" } \ No newline at end of file diff --git a/scripts/utils/optimismTestnet-contracts.json b/scripts/utils/optimismTestnet-contracts.json new file mode 100644 index 0000000..ab2f5fc --- /dev/null +++ b/scripts/utils/optimismTestnet-contracts.json @@ -0,0 +1,3 @@ +{ + "LZGatedProxy": "0xcf86ce09824412e84eea1467F362BEF37fcf4654" +} \ No newline at end of file diff --git a/tasks/deploy-token-remote.ts b/tasks/deploy-token-remote.ts index a5f03ed..ceb62d5 100644 --- a/tasks/deploy-token-remote.ts +++ b/tasks/deploy-token-remote.ts @@ -1,5 +1,5 @@ import { task } from 'hardhat/config'; -import { LZ_DESTINATION_CHAINS, LZ_CONFIG } from './helpers/constants'; +import { LZ_CONFIG } from './helpers/constants'; import deployContract from './helpers/deployContract'; task('deploy-token-remote', 'deploys OmniSBT on the destination chain').setAction(async ({}, hre) => { diff --git a/tasks/estimate-fee.ts b/tasks/estimate-fee.ts index ff5a448..c719208 100644 --- a/tasks/estimate-fee.ts +++ b/tasks/estimate-fee.ts @@ -22,12 +22,18 @@ task('estimate-fee', 'estimate the fee of sending message from source chain to d [await deployer.getAddress(), 1, 1, 'ipfs://QmfJ3ET9FjpV4X9oTi5T2aDZjg4h3pnwr7Au242c5RZH6b'] ); + const ESTIMATED_GAS = 0; + const adapterParams = ethers.utils.defaultAbiCoder.encode( + ['uint16', 'uint256'], + [1, ESTIMATED_GAS] + ); + const fees = await endpoint.estimateFees( LZ_CONFIG[destination].chainId, // the destination LayerZero chainId contractsDeployed.OmniSBT, // your contract address that calls Endpoint.send() payload, false, // _payInZRO - "0x" // default '0x' adapterParams, see: Relayer Adapter Param docs + adapterParams // https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters ); console.log('payload types', types); diff --git a/tasks/gated-modules/deploy-modules-source.ts b/tasks/gated-modules/deploy-modules-source.ts new file mode 100644 index 0000000..fce1377 --- /dev/null +++ b/tasks/gated-modules/deploy-modules-source.ts @@ -0,0 +1,58 @@ +import { task } from 'hardhat/config'; +import { LZ_CONFIG_GATED_MODULES } from './../helpers/constants'; +import deployContract from './../helpers/deployContract'; +import { getLensHubDeployed, getMockSandboxGovernance } from './../helpers/lens'; +import getContract from './../helpers/getContract'; + +task('deploy-modules-source', 'deploys our Lens modules on the source chain').setAction(async ({}, hre) => { + const ethers = hre.ethers; + const networkName = hre.network.name; + const [deployer] = await ethers.getSigners(); + + if (!LZ_CONFIG_GATED_MODULES[networkName]) throw new Error('invalid network'); + + const key = networkName === 'mumbai' ? 'lensHub sandbox' : 'lensHub proxy'; // using sandbox for module whitelisting + const lensHub = getLensHubDeployed(key, networkName, deployer.provider); + + const followModule = await deployContract( + ethers, + networkName, + 'LZGatedFollowModule', + [lensHub.address, LZ_CONFIG_GATED_MODULES[networkName].endpoint, [], []] + ); + + const referenceModule = await deployContract( + ethers, + networkName, + 'LZGatedReferenceModule', + [lensHub.address, LZ_CONFIG_GATED_MODULES[networkName].endpoint, [], []] + ); + + const collectModule = await deployContract( + ethers, + networkName, + 'LZGatedCollectModule', + [lensHub.address, LZ_CONFIG_GATED_MODULES[networkName].endpoint, [], []] + ); + + if (networkName === 'mumbai') { + const mockSandboxGovernance = getMockSandboxGovernance(deployer.provider); + console.log(`whitelisting modules through mock sandbox governance: ${mockSandboxGovernance.address}`); + + let tx; + console.log('mockSandboxGovernance.whitelistFollowModule()'); + tx = await mockSandboxGovernance.connect(deployer).whitelistFollowModule(followModule.address, true, { gasLimit: 100000 }); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + + console.log('mockSandboxGovernance.whitelistReferenceModule()'); + tx = await mockSandboxGovernance.connect(deployer).whitelistReferenceModule(referenceModule.address, true, { gasLimit: 100000 }); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + + console.log('mockSandboxGovernance.whitelistCollectModule()'); + tx = await mockSandboxGovernance.connect(deployer).whitelistCollectModule(collectModule.address, true, { gasLimit: 100000 }); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + } +}); diff --git a/tasks/gated-modules/deploy-proxy-remote.ts b/tasks/gated-modules/deploy-proxy-remote.ts new file mode 100644 index 0000000..6a9b785 --- /dev/null +++ b/tasks/gated-modules/deploy-proxy-remote.ts @@ -0,0 +1,31 @@ +import { task } from 'hardhat/config'; +import { LZ_CONFIG_GATED_MODULES } from './../helpers/constants'; +import deployContract from './../helpers/deployContract'; +import { contractsDeployedOn } from './../../scripts/utils/migrations'; + +task('deploy-proxy-remote', 'deploys LZGatedProxy on a remote chain').setAction(async ({}, hre) => { + const ethers = hre.ethers; + const networkName = hre.network.name; + const [deployer] = await ethers.getSigners(); + + if (!LZ_CONFIG_GATED_MODULES[networkName]) throw new Error('invalid network'); + + const sourceNetwork = LZ_CONFIG_GATED_MODULES[networkName].remote; + + // modules deployed on the source chain + const contracts = contractsDeployedOn(sourceNetwork); + + console.log(`deploying LZGatedProxy on remote: ${networkName} with source: ${sourceNetwork}...`); + const lzGatedProxy = await deployContract( + ethers, + networkName, + 'LZGatedProxy', + [ + LZ_CONFIG_GATED_MODULES[networkName].endpoint, + LZ_CONFIG_GATED_MODULES[sourceNetwork].chainId, + contracts.LZGatedFollowModule, + contracts.LZGatedReferenceModule, + contracts.LZGatedCollectModule + ] + ); +}); diff --git a/tasks/gated-modules/estimate-fee-gated.ts b/tasks/gated-modules/estimate-fee-gated.ts new file mode 100644 index 0000000..8d5dd68 --- /dev/null +++ b/tasks/gated-modules/estimate-fee-gated.ts @@ -0,0 +1,72 @@ +import { task } from 'hardhat/config'; +import { Contract, utils, Bytes, providers, BigNumber } from 'ethers'; +import { contractsDeployed } from './../../scripts/utils/migrations'; +import ILayerZeroMessagingLibrary from './../helpers/ILayerZeroMessagingLibrary.json'; +import { getLensHubDeployed, getFollowWithSigParts } from './../helpers/lens'; +import { MAX_UINT256 } from './../../test/lens/helpers/constants'; +import { + LZ_CONFIG_GATED_MODULES, + SANDBOX_USER_PROFILE_ID, + TOKEN_CONTRACT, + TOKEN_THRESHOLD, +} from './../helpers/constants'; + +const { ALCHEMY_MUMBAI_URL, ALCHEMY_POLYGON_URL } = process.env; + +// the same can be done for LZGatedCollectModule + LZGatedReferenceModule, just need to setup the correct sig data +task('estimate-fee-gated', 'estimate the fee of sending the followSig from source chain to remote chain').setAction(async ({}, hre) => { + const ethers = hre.ethers; + const networkName = hre.network.name; + const [deployer] = await ethers.getSigners(); + + if (!(LZ_CONFIG_GATED_MODULES[networkName] && LZ_CONFIG_GATED_MODULES[networkName].remote)) throw new Error('invalid network'); + + const destination = LZ_CONFIG_GATED_MODULES[networkName].remote; + const provider = new providers.JsonRpcProvider(destination === 'mumbai' ? ALCHEMY_MUMBAI_URL : ALCHEMY_POLYGON_URL); + const lensHub = await getLensHubDeployed('lensHub sandbox', destination, provider); + const endpoint = new Contract(LZ_CONFIG_GATED_MODULES[networkName].endpoint, ILayerZeroMessagingLibrary.abi, deployer.provider); + + const followerAddress = await deployer.getAddress(); + const nonce = (await lensHub.sigNonces(followerAddress)).toNumber(); + const chainId = parseInt(await hre.getChainId()); + + const followWithSigData = await getFollowWithSigParts({ + chainId, + wallet: deployer, + lensHubAddress: lensHub.address, + profileIds: [BigNumber.from(SANDBOX_USER_PROFILE_ID)], + datas: [[]], + nonce, + deadline: MAX_UINT256, + follower: followerAddress, + }); + + // example payload + const followWithSigType = 'tuple(address follower, uint256[] profileIds, bytes[] datas, tuple(uint8 v, bytes32 r, bytes32 s, uint256 deadline) sig) followSig'; + const types = ['address', 'uint256', followWithSigType]; + const payload = ethers.utils.defaultAbiCoder.encode( + types, + [TOKEN_CONTRACT, TOKEN_THRESHOLD, followWithSigData] + ); + + console.log(`networkName: ${networkName}`); + console.log(`endpoint.address: ${endpoint.address}`); + + const ESTIMATED_GAS_REMOTE = 500_000 // based on some tests... + console.log(`ESTIMATED_GAS_REMOTE: ${ESTIMATED_GAS_REMOTE}`); + const adapterParams = ethers.utils.solidityPack( + ['uint16', 'uint256'], + [1, ESTIMATED_GAS_REMOTE] + ); + + const fees = await endpoint.estimateFees( + LZ_CONFIG_GATED_MODULES[destination].chainId, // the destination LayerZero chainId + contractsDeployed.LZGatedProxy, // your contract address that calls Endpoint.send() + payload, + false, // _payInZRO + adapterParams // https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters + ); + + console.log('payload types: ', types); + console.log(`fees in ${['mumbai', 'polygon'].includes(networkName) ? 'matic' : 'ether'}`, utils.formatEther(fees[0])); +}); diff --git a/tasks/gated-modules/force-resume-receive.ts b/tasks/gated-modules/force-resume-receive.ts new file mode 100644 index 0000000..8ba6277 --- /dev/null +++ b/tasks/gated-modules/force-resume-receive.ts @@ -0,0 +1,27 @@ +import { task } from 'hardhat/config'; +import { LZ_CONFIG_GATED_MODULES } from './../helpers/constants'; +import getContract from './../helpers/getContract'; +import { contractsDeployedOn } from './../../scripts/utils/migrations'; + +// worst case, in the case of a revert +task('force-resume-receive', 'force our lz contract to receive new messages after a revert').setAction(async ({}, hre) => { + const ethers = hre.ethers; + const networkName = hre.network.name; + const [deployer] = await ethers.getSigners(); + + if (!LZ_CONFIG_GATED_MODULES[networkName]) throw new Error('invalid network'); + + const followModule = await getContract(ethers, 'LZGatedFollowModule', deployer); + // const referenceModule = await getContract(ethers, 'LZGatedReferenceModule', deployer); + // const collectModule = await getContract(ethers, 'LZGatedCollectModule', deployer); + + const REMOTE = 'goerli'; + const { LZGatedProxy } = contractsDeployedOn(REMOTE); + + const trustedRemote = ethers.utils.solidityPack(['address','address'], [LZGatedProxy, followModule.address]); + const tx = await followModule.forceResumeReceive(LZ_CONFIG_GATED_MODULES[REMOTE].chainId, trustedRemote); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + + console.log('done!'); +}); diff --git a/tasks/gated-modules/set-follow-module.ts b/tasks/gated-modules/set-follow-module.ts new file mode 100644 index 0000000..c8cb55b --- /dev/null +++ b/tasks/gated-modules/set-follow-module.ts @@ -0,0 +1,47 @@ +import { task } from 'hardhat/config'; +import { + LZ_CONFIG_GATED_MODULES, + SANDBOX_USER_PROFILE_ID, + TOKEN_CONTRACT, + TOKEN_THRESHOLD, + TOKEN_CHAIN_ID, +} from './../helpers/constants'; +import { getLensHubDeployed } from './../helpers/lens'; +import { contractsDeployed } from './../../scripts/utils/migrations'; +import getContract from './../helpers/getContract'; + +task('set-follow-module', 'sets the LZGatedFollowModule on our test profile').setAction(async ({}, hre) => { + const ethers = hre.ethers; + const networkName = hre.network.name; + const [deployer] = await ethers.getSigners(); + + if (!LZ_CONFIG_GATED_MODULES[networkName]) throw new Error('invalid network'); + + // using the sandbox deployment for module whitelisting + const lensHub = await getLensHubDeployed('lensHub sandbox', networkName, deployer.provider); + + const followModule = await getContract(ethers, 'LZGatedFollowModule', deployer); + + // tokenContract, balanceThreshold, chainId + const data = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [TOKEN_CONTRACT, TOKEN_THRESHOLD, TOKEN_CHAIN_ID] + ); + + // `SANDBOX_USER_PROFILE_ID` profile was created thru MockProfileCreationProxy, owned by `deployer` + // https://docs.lens.xyz/docs/deployed-contract-addresses#sandbox-mumbai-testnet-addresses + + console.log('lensHub.setFollowModule()') + const tx = await lensHub.connect(deployer).setFollowModule( + SANDBOX_USER_PROFILE_ID, + contractsDeployed.LZGatedFollowModule, + data, + { gasLimit: 210000 } + ); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + + console.log('follow module set!'); + const res = await followModule.gatedFollowPerProfile(SANDBOX_USER_PROFILE_ID); + console.log(res); +}); diff --git a/tasks/gated-modules/set-trusted-remotes.ts b/tasks/gated-modules/set-trusted-remotes.ts new file mode 100644 index 0000000..1f32459 --- /dev/null +++ b/tasks/gated-modules/set-trusted-remotes.ts @@ -0,0 +1,49 @@ +import { task } from 'hardhat/config'; +import promiseLimit from 'promise-limit'; +import { LZ_CONFIG_GATED_MODULES } from './../helpers/constants'; +import getContract from './../helpers/getContract'; +import { contractsDeployedOn } from './../../scripts/utils/migrations'; + +// https://layerzero.gitbook.io/docs/evm-guides/master/set-trusted-remotes +task('set-trusted-remotes', 'sets the trusted remotes for each module / remote pair').setAction(async ({}, hre) => { + const ethers = hre.ethers; + const networkName = hre.network.name; + const [deployer] = await ethers.getSigners(); + + const limit = promiseLimit(1); + + if (!LZ_CONFIG_GATED_MODULES[networkName]) throw new Error('invalid network'); + + const followModule = await getContract(ethers, 'LZGatedFollowModule', deployer); + const referenceModule = await getContract(ethers, 'LZGatedReferenceModule', deployer); + const collectModule = await getContract(ethers, 'LZGatedCollectModule', deployer); + + const { remotes } = LZ_CONFIG_GATED_MODULES[networkName]; + + await Promise.all(remotes.map((remote) => limit(async () => { + const { LZGatedProxy } = contractsDeployedOn(remote); + + if (!LZGatedProxy) throw new Error(`missing LZGatedProxy at remote: ${remote}`); + + console.log(`setting trusted remote (${LZ_CONFIG_GATED_MODULES[remote].chainId}, ${LZGatedProxy})`); + + let tx; + let trustedRemote; + trustedRemote = ethers.utils.solidityPack(['address','address'], [LZGatedProxy, followModule.address]); + tx = await followModule.setTrustedRemote(LZ_CONFIG_GATED_MODULES[remote].chainId, trustedRemote); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + + trustedRemote = ethers.utils.solidityPack(['address','address'], [LZGatedProxy, referenceModule.address]); + tx = await referenceModule.setTrustedRemote(LZ_CONFIG_GATED_MODULES[remote].chainId, trustedRemote); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + + trustedRemote = ethers.utils.solidityPack(['address','address'], [LZGatedProxy, collectModule.address]); + tx = await collectModule.setTrustedRemote(LZ_CONFIG_GATED_MODULES[remote].chainId, LZGatedProxy); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + }))); + + console.log('done!'); +}); diff --git a/tasks/gated-modules/test-follow-module.ts b/tasks/gated-modules/test-follow-module.ts new file mode 100644 index 0000000..1981c13 --- /dev/null +++ b/tasks/gated-modules/test-follow-module.ts @@ -0,0 +1,66 @@ +import { task } from 'hardhat/config'; +import { Contract, utils, Bytes, providers, BigNumber } from 'ethers'; +import getContract from './../helpers/getContract'; +import { getLensHubDeployed, getFollowWithSigParts } from './../helpers/lens'; +import { MAX_UINT256 } from './../../test/lens/helpers/constants'; +import { + LZ_CONFIG_GATED_MODULES, + SANDBOX_USER_PROFILE_ID, + SANDBOX_USER_PROFILE_ID_USER, + TOKEN_CONTRACT, + TOKEN_THRESHOLD, +} from './../helpers/constants'; + +const { ALCHEMY_MUMBAI_URL, ALCHEMY_POLYGON_URL } = process.env; + +const ESTIMATED_FOLLOW_FEE_GWEI = '2500'; // derived from `npx hardhat estimate-fee-gated` +const ESTIMATED_GAS = 250000; +const REMOTE_CHAIN_IDS = { 'mumbai': 80001, 'polygon': 137 }; + +// the same can be done for LZGatedCollectModule + LZGatedReferenceModule, just need to setup the correct sig data +task('test-follow-module', 'try to folllow a profile which has set their follow module to LZGatedFollowModule').setAction(async ({}, hre) => { + const ethers = hre.ethers; + const networkName = hre.network.name; + const [deployer, governance, user] = await ethers.getSigners(); + + if (!(LZ_CONFIG_GATED_MODULES[networkName] && LZ_CONFIG_GATED_MODULES[networkName].remote)) throw new Error('invalid network'); + + const destination = 'mumbai'; //LZ_CONFIG_GATED_MODULES[networkName].remote; + const provider = new providers.JsonRpcProvider(destination === 'mumbai' ? ALCHEMY_MUMBAI_URL : ALCHEMY_POLYGON_URL); + const lensHub = await getLensHubDeployed('lensHub sandbox', destination, provider); + const lzGatedProxy = await getContract(ethers, 'LZGatedProxy', deployer); + + const followerAddress = await governance.getAddress(); + const nonce = (await lensHub.sigNonces(followerAddress)).toNumber(); + const chainId = REMOTE_CHAIN_IDS[destination]; + + const followWithSigData = await getFollowWithSigParts({ + chainId, + wallet: governance, + lensHubAddress: lensHub.address, + profileIds: [SANDBOX_USER_PROFILE_ID], + datas: [[]], + nonce, + deadline: MAX_UINT256, + follower: followerAddress, + }); + + console.log(`followWithSigData:`); + console.log(JSON.stringify(followWithSigData,null,2)); + + const ESTIMATED_GAS_REMOTE = 500_000 // based on some tests... + const GAS_LIMIT = 250_000; + console.log('lzGatedProxy.relayFollowWithSig()'); + const tx = await lzGatedProxy.relayFollowWithSig( + TOKEN_CONTRACT, + TOKEN_THRESHOLD, + followWithSigData, + ESTIMATED_GAS_REMOTE, + { value: utils.parseUnits(ESTIMATED_FOLLOW_FEE_GWEI, 'gwei'), gasLimit: GAS_LIMIT } + ); + console.log(`tx: ${tx.hash}`); + await tx.wait(); + + // assuming `followerAddress` has a balance of `TOKEN_CONTRACT` >= `TOKEN_THRESHOLD` - likely good + // check the latext tx against the deployed LZGatedFollowModule +}); diff --git a/tasks/helpers/abi/LensHub.json b/tasks/helpers/abi/LensHub.json new file mode 100644 index 0000000..25f846b --- /dev/null +++ b/tasks/helpers/abi/LensHub.json @@ -0,0 +1,2609 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "LensHub", + "sourceName": "contracts/core/LensHub.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "followNFTImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "collectNFTImpl", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "CallerNotCollectNFT", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotFollowNFT", + "type": "error" + }, + { + "inputs": [], + "name": "CannotInitImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "EmergencyAdminCannotUnpause", + "type": "error" + }, + { + "inputs": [], + "name": "InitParamsInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "Initialized", + "type": "error" + }, + { + "inputs": [], + "name": "NotGovernance", + "type": "error" + }, + { + "inputs": [], + "name": "NotGovernanceOrEmergencyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "NotOwnerOrApproved", + "type": "error" + }, + { + "inputs": [], + "name": "NotProfileOwner", + "type": "error" + }, + { + "inputs": [], + "name": "NotProfileOwnerOrDispatcher", + "type": "error" + }, + { + "inputs": [], + "name": "Paused", + "type": "error" + }, + { + "inputs": [], + "name": "ProfileCreatorNotWhitelisted", + "type": "error" + }, + { + "inputs": [], + "name": "ProfileImageURILengthInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "PublicationDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "PublishingPaused", + "type": "error" + }, + { + "inputs": [], + "name": "SignatureExpired", + "type": "error" + }, + { + "inputs": [], + "name": "SignatureInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "name": "burnWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "collect", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.CollectWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "collectWithSig", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "contentURI", + "type": "string" + }, + { + "internalType": "uint256", + "name": "profileIdPointed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubIdPointed", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "referenceModuleData", + "type": "bytes" + }, + { + "internalType": "address", + "name": "collectModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "collectModuleInitData", + "type": "bytes" + }, + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "referenceModuleInitData", + "type": "bytes" + } + ], + "internalType": "struct DataTypes.CommentData", + "name": "vars", + "type": "tuple" + } + ], + "name": "comment", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "contentURI", + "type": "string" + }, + { + "internalType": "uint256", + "name": "profileIdPointed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubIdPointed", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "referenceModuleData", + "type": "bytes" + }, + { + "internalType": "address", + "name": "collectModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "collectModuleInitData", + "type": "bytes" + }, + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "referenceModuleInitData", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.CommentWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "commentWithSig", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "string", + "name": "handle", + "type": "string" + }, + { + "internalType": "string", + "name": "imageURI", + "type": "string" + }, + { + "internalType": "address", + "name": "followModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "followModuleInitData", + "type": "bytes" + }, + { + "internalType": "string", + "name": "followNFTURI", + "type": "string" + } + ], + "internalType": "struct DataTypes.CreateProfileData", + "name": "vars", + "type": "tuple" + } + ], + "name": "createProfile", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "defaultProfile", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "collectNFTId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "emitCollectNFTTransferEvent", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "followNFTId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "emitFollowNFTTransferEvent", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "exists", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "profileIds", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "datas", + "type": "bytes[]" + } + ], + "name": "follow", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "follower", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "profileIds", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "datas", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.FollowWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "followWithSig", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + } + ], + "name": "getCollectModule", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + } + ], + "name": "getCollectNFT", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCollectNFTImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + } + ], + "name": "getContentURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + } + ], + "name": "getDispatcher", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getDomainSeparator", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + } + ], + "name": "getFollowModule", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + } + ], + "name": "getFollowNFT", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFollowNFTImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + } + ], + "name": "getFollowNFTURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGovernance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + } + ], + "name": "getHandle", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + } + ], + "name": "getProfile", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "pubCount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "followModule", + "type": "address" + }, + { + "internalType": "address", + "name": "followNFT", + "type": "address" + }, + { + "internalType": "string", + "name": "handle", + "type": "string" + }, + { + "internalType": "string", + "name": "imageURI", + "type": "string" + }, + { + "internalType": "string", + "name": "followNFTURI", + "type": "string" + } + ], + "internalType": "struct DataTypes.ProfileStruct", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "handle", + "type": "string" + } + ], + "name": "getProfileIdByHandle", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + } + ], + "name": "getPub", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileIdPointed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubIdPointed", + "type": "uint256" + }, + { + "internalType": "string", + "name": "contentURI", + "type": "string" + }, + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + }, + { + "internalType": "address", + "name": "collectModule", + "type": "address" + }, + { + "internalType": "address", + "name": "collectNFT", + "type": "address" + } + ], + "internalType": "struct DataTypes.PublicationStruct", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + } + ], + "name": "getPubCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + } + ], + "name": "getPubPointer", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + } + ], + "name": "getPubType", + "outputs": [ + { + "internalType": "enum DataTypes.PubType", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubId", + "type": "uint256" + } + ], + "name": "getReferenceModule", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getState", + "outputs": [ + { + "internalType": "enum DataTypes.ProtocolState", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "address", + "name": "newGovernance", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collectModule", + "type": "address" + } + ], + "name": "isCollectModuleWhitelisted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "followModule", + "type": "address" + } + ], + "name": "isFollowModuleWhitelisted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "profileCreator", + "type": "address" + } + ], + "name": "isProfileCreatorWhitelisted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + } + ], + "name": "isReferenceModuleWhitelisted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "mintTimestampOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "profileIdPointed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubIdPointed", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "referenceModuleData", + "type": "bytes" + }, + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "referenceModuleInitData", + "type": "bytes" + } + ], + "internalType": "struct DataTypes.MirrorData", + "name": "vars", + "type": "tuple" + } + ], + "name": "mirror", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "profileIdPointed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pubIdPointed", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "referenceModuleData", + "type": "bytes" + }, + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "referenceModuleInitData", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.MirrorWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "mirrorWithSig", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "name": "permitForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "contentURI", + "type": "string" + }, + { + "internalType": "address", + "name": "collectModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "collectModuleInitData", + "type": "bytes" + }, + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "referenceModuleInitData", + "type": "bytes" + } + ], + "internalType": "struct DataTypes.PostData", + "name": "vars", + "type": "tuple" + } + ], + "name": "post", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "contentURI", + "type": "string" + }, + { + "internalType": "address", + "name": "collectModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "collectModuleInitData", + "type": "bytes" + }, + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "referenceModuleInitData", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.PostWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "postWithSig", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + } + ], + "name": "setDefaultProfile", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.SetDefaultProfileWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "setDefaultProfileWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "dispatcher", + "type": "address" + } + ], + "name": "setDispatcher", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "dispatcher", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.SetDispatcherWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "setDispatcherWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newEmergencyAdmin", + "type": "address" + } + ], + "name": "setEmergencyAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "followModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "followModuleInitData", + "type": "bytes" + } + ], + "name": "setFollowModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "followModule", + "type": "address" + }, + { + "internalType": "bytes", + "name": "followModuleInitData", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.SetFollowModuleWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "setFollowModuleWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "followNFTURI", + "type": "string" + } + ], + "name": "setFollowNFTURI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "followNFTURI", + "type": "string" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.SetFollowNFTURIWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "setFollowNFTURIWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newGovernance", + "type": "address" + } + ], + "name": "setGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "imageURI", + "type": "string" + } + ], + "name": "setProfileImageURI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "profileId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "imageURI", + "type": "string" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct DataTypes.EIP712Signature", + "name": "sig", + "type": "tuple" + } + ], + "internalType": "struct DataTypes.SetProfileImageURIWithSigData", + "name": "vars", + "type": "tuple" + } + ], + "name": "setProfileImageURIWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum DataTypes.ProtocolState", + "name": "newState", + "type": "uint8" + } + ], + "name": "setState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "sigNonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenDataOf", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint96", + "name": "mintTimestamp", + "type": "uint96" + } + ], + "internalType": "struct IERC721Time.TokenData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collectModule", + "type": "address" + }, + { + "internalType": "bool", + "name": "whitelist", + "type": "bool" + } + ], + "name": "whitelistCollectModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "followModule", + "type": "address" + }, + { + "internalType": "bool", + "name": "whitelist", + "type": "bool" + } + ], + "name": "whitelistFollowModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "profileCreator", + "type": "address" + }, + { + "internalType": "bool", + "name": "whitelist", + "type": "bool" + } + ], + "name": "whitelistProfileCreator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "referenceModule", + "type": "address" + }, + { + "internalType": "bool", + "name": "whitelist", + "type": "bool" + } + ], + "name": "whitelistReferenceModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60e06040526000600b553480156200001657600080fd5b50604051620060af380380620060af8339810160408190526200003991620000c2565b306080526001600160a01b03821662000065576040516348be0eb360e01b815260040160405180910390fd5b6001600160a01b0381166200008d576040516348be0eb360e01b815260040160405180910390fd5b6001600160a01b0391821660a0521660c052620000fa565b80516001600160a01b0381168114620000bd57600080fd5b919050565b60008060408385031215620000d657600080fd5b620000e183620000a5565b9150620000f160208401620000a5565b90509250929050565b60805160a05160c051615f77620001386000396000818161082d01528181611fb301526127c30152600061061e01526000610d7c0152615f776000f3fe608060405234801561001057600080fd5b50600436106104805760003560e01c806384114ad411610257578063bd12d3f011610146578063dd69cdb1116100c3578063f08f4f6411610087578063f08f4f6414610c21578063f1b2f8bc14610c41578063f990ccd714610c54578063fb78ae6c14610c74578063ffea138e14610c8757600080fd5b8063dd69cdb114610ba4578063df457c3414610bb7578063e985e9c514610bca578063ec81d19414610c06578063ed24911d14610c1957600080fd5b8063c6b5d06c1161010a578063c6b5d06c14610b1f578063c736c99014610b32578063c87b56dd14610b52578063d923d20c14610b65578063dc21725314610b9157600080fd5b8063bd12d3f014610a93578063bdfdd4bc14610aa6578063bfbf0b4b14610ab9578063bfd24f4714610acc578063c0da9bcd14610adf57600080fd5b8063a6d8e1e4116101d4578063b48951e411610198578063b48951e414610a10578063b5a3149614610a23578063b6f32d2b14610a36578063b7984c0514610a49578063b88d4fde14610a8057600080fd5b8063a6d8e1e414610966578063a9ec656314610979578063ab033ea9146109a5578063ad3e72bf146109b8578063af05dd22146109e457600080fd5b806392254a621161021b57806392254a62146108fc57806395d89b4114610925578063963ff1411461092d5780639b84a14c14610940578063a22cb4651461095357600080fd5b806384114ad41461087757806386e2947b1461088a57806389028a131461089d5780638e204fb4146108b05780638e4fd6a9146108dc57600080fd5b80633a15ff0711610373578063540528b9116102f05780636dea40b3116102b45780636dea40b31461080557806370a082311461081857806377349a5f1461082b5780637a375716146108515780637ef67f991461086457600080fd5b8063540528b91461075757806356de96db1461078057806357ff49f6146107935780635ca3eebf146107ca5780636352211e146107f257600080fd5b806342966c681161033757806342966c68146106d45780634f558e79146106e75780634f6ccce7146106fa57806350ddf35c1461070d57806352aaef551461072057600080fd5b80633a15ff07146106685780633b28b89f146106885780633b5081321461069b5780634187e4c5146106ae57806342842e0e146106c157600080fd5b806320fa728a1161040157806331dcebe3116103c557806331dcebe3146105e957806331fff07c146105fc5780633502ac4b1461061c57806335da339414610642578063374c94731461065557600080fd5b806320fa728a1461058c57806323b872dd1461059f578063289b3c0d146105b25780632f745c59146105c35780632faeed81146105d657600080fd5b8063095ea7b311610448578063095ea7b31461051557806318160ddd146105285780631865c57d1461053a5780631cbb26201461054d578063209055061461057957600080fd5b806301ffc9a714610485578063054871b8146104ad57806306fdde03146104c2578063077f224a146104d7578063081812fc146104ea575b600080fd5b610498610493366004614904565b610c9a565b60405190151581526020015b60405180910390f35b6104c06104bb366004614962565b610cc5565b005b6104ca610ce6565b6040516104a49190614a05565b6104c06104e5366004614a2f565b610d78565b6104fd6104f8366004614aaf565b610e12565b6040516001600160a01b0390911681526020016104a4565b6104c0610523366004614ac8565b610eac565b6008545b6040519081526020016104a4565b600c5460ff166040516104a49190614b08565b61049861055b366004614b22565b6001600160a01b03166000908152600e602052604090205460ff1690565b6104c0610587366004614b4d565b610fbd565b61052c61059a366004614b80565b611028565b6104c06105ad366004614bc1565b61105a565b6017546001600160a01b03166104fd565b61052c6105d1366004614ac8565b61108b565b61052c6105e4366004614c15565b611121565b6104c06105f7366004614b4d565b61114b565b61060f61060a366004614c49565b6111ae565b6040516104a49190614c6b565b7f00000000000000000000000000000000000000000000000000000000000000006104fd565b6104c0610650366004614b22565b611237565b6104ca610663366004614aaf565b6112a4565b61052c610676366004614aaf565b60009081526013602052604090205490565b6104c0610696366004614c91565b611349565b61052c6106a9366004614cd8565b6114e5565b6104c06106bc366004614b4d565b61172d565b6104c06106cf366004614bc1565b611790565b6104c06106e2366004614aaf565b6117ab565b6104986106f5366004614aaf565b6117c8565b61052c610708366004614aaf565b6117e7565b61052c61071b366004614aaf565b61187a565b6104fd61072e366004614c49565b60009182526014602090815260408084209284529190529020600501546001600160a01b031690565b6104fd610765366004614aaf565b6000908152601160205260409020546001600160a01b031690565b6104c061078e366004614d0c565b611910565b6104fd6107a1366004614c49565b60009182526014602090815260408084209284529190529020600401546001600160a01b031690565b6107dd6107d8366004614c49565b611997565b604080519283526020830191909152016104a4565b6104fd610800366004614aaf565b6119c0565b6104c0610813366004614d2d565b611a37565b61052c610826366004614b22565b611ace565b7f00000000000000000000000000000000000000000000000000000000000000006104fd565b61052c61085f366004614d86565b611b55565b6104c0610872366004614dd3565b611ea1565b61052c610885366004614e10565b611f70565b6104c0610898366004614e4a565b61202d565b6104c06108ab366004614e8e565b6120cb565b6104986108be366004614b22565b6001600160a01b031660009081526010602052604090205460ff1690565b6108ef6108ea366004614c91565b612190565b6040516104a49190614ee3565b61052c61090a366004614b22565b6001600160a01b031660009081526015602052604090205490565b6104ca612416565b61052c61093b366004614c15565b612425565b6104c061094e366004614c15565b6124ef565b6104c0610961366004614b4d565b6125cb565b6104c0610974366004614b4d565b61262f565b6104fd610987366004614aaf565b6000908152601360205260409020600201546001600160a01b031690565b6104c06109b3366004614b22565b612692565b6104986109c6366004614b22565b6001600160a01b03166000908152600f602052604090205460ff1690565b6104986109f2366004614b22565b6001600160a01b03166000908152600d602052604090205460ff1690565b61052c610a1e366004614f27565b6126a3565b6104ca610a31366004614c49565b61284a565b61052c610a44366004614f62565b612912565b6104fd610a57366004614c49565b60009182526014602090815260408084209284529190529020600301546001600160a01b031690565b6104c0610a8e3660046150ab565b612937565b6104c0610aa1366004614c15565b612969565b6104c0610ab4366004615112565b6129cc565b6104c0610ac736600461514d565b612a5b565b6104c0610ada366004615169565b612b1a565b610af2610aed366004614aaf565b612b35565b6040805182516001600160a01b031681526020928301516001600160601b031692810192909252016104a4565b6104c0610b2d366004614962565b612c03565b610b45610b40366004614c49565b612c1f565b6040516104a4919061518c565b6104ca610b60366004614aaf565b612d7e565b6104fd610b73366004614aaf565b6000908152601360205260409020600101546001600160a01b031690565b6104c0610b9f36600461514d565b612eb0565b6104c0610bb23660046151f6565b612f93565b61052c610bc5366004614cd8565b612fae565b610498610bd836600461521a565b6001600160a01b03918216600090815260056020908152604080832093909416825291909152205460ff1690565b6104ca610c14366004614aaf565b6131a5565b61052c6131c5565b610c34610c2f366004614aaf565b6131d4565b6040516104a49190615244565b6104c0610c4f366004614aaf565b613428565b61052c610c62366004614b22565b600a6020526000908152604090205481565b6108ef610c82366004615315565b61343a565b61052c610c95366004614c15565b6134ce565b60006001600160e01b0319821663780e9d6360e01b1480610cbf5750610cbf826135a2565b92915050565b610ccd6135f2565b610cd68361362c565b610ce183838361368b565b505050565b606060008054610cf590615374565b80601f0160208091040260200160405190810160405280929190818152602001828054610d2190615374565b8015610d6e5780601f10610d4357610100808354040283529160200191610d6e565b820191906000526020600020905b815481529060010190602001808311610d5157829003601f168201915b5050505050905090565b60017f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316301415610dc4576040516325c7410560e21b815260040160405180910390fd5b600b548111610de6576040516302ed543d60e51b815260040160405180910390fd5b600b819055610df78686868661370c565b610e01600261375d565b610e0a826137db565b505050505050565b6000818152600260205260408120546001600160a01b0316610e905760405162461bcd60e51b815260206004820152602c60248201527f4552433732313a20617070726f76656420717565727920666f72206e6f6e657860448201526b34b9ba32b73a103a37b5b2b760a11b60648201526084015b60405180910390fd5b506000908152600460205260409020546001600160a01b031690565b6000610eb7826119c0565b9050806001600160a01b0316836001600160a01b03161415610f255760405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e656044820152603960f91b6064820152608401610e87565b336001600160a01b0382161480610f415750610f418133610bd8565b610fb35760405162461bcd60e51b815260206004820152603860248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f74206f7760448201527f6e6572206e6f7220617070726f76656420666f7220616c6c00000000000000006064820152608401610e87565b610ce18383613834565b610fc56138a2565b6001600160a01b0382166000818152600d6020908152604091829020805460ff191685151590811790915591514281529192917f8f617843889b94892bd44852d36ca6a7f49ecf4350a01e7b68e22d80f4ed95bc91015b60405180910390a35050565b600080838360405161103b9291906153a9565b6040805191829003909120600090815260126020522054949350505050565b61106433826138cd565b6110805760405162461bcd60e51b8152600401610e87906153b9565b610ce18383836139c0565b600061109683611ace565b82106110f85760405162461bcd60e51b815260206004820152602b60248201527f455243373231456e756d657261626c653a206f776e657220696e646578206f7560448201526a74206f6620626f756e647360a81b6064820152608401610e87565b506001600160a01b03919091166000908152600660209081526040808320938352929052205490565b600061112b613b6b565b611135823561362c565b610cbf6111418361540a565b613ba2565b919050565b6111536138a2565b6001600160a01b0382166000818152600f6020908152604091829020805460ff191685151590811790915591514281529192917f6cc19a794d6a439023150cd58748eed4353190c0bb060d2e6250e2df4a68b673910161101c565b60008115806111ca575060008381526013602052604090205482115b156111d757506003610cbf565b60008381526014602090815260408083208584529091529020600401546001600160a01b031661120957506002610cbf565b600083815260146020908152604080832085845290915290205461122f57506000610cbf565b506001610cbf565b61123f6138a2565b601880546001600160a01b038381166001600160a01b03198316811790935560405191169190829033907f676c0801b0f400762e958ee31cfbb10870e70786f6761f57c8647e766b0db3d9906112989042815260200190565b60405180910390a45050565b60008181526013602052604090206005018054606091906112c490615374565b80601f01602080910402602001604051908101604052809291908181526020018280546112f090615374565b801561133d5780601f106113125761010080835404028352916020019161133d565b820191906000526020600020905b81548152906001019060200180831161132057829003601f168201915b50505050509050919050565b6113516135f2565b600061135d82356119c0565b905061144761143d7f61332f0343256b019dca2e0ffc75766fde55292d645df285bb1a3b74a493b51084356113986040870160208801614b22565b6113a560408801886154a5565b6040516113b39291906153a9565b604080519182900382206001600160a01b0389166000908152600a6020908152929020805460018101909155611422969594939192909160c08c0135910195865260208601949094526001600160a01b039290921660408501526060840152608083015260a082015260c00190565b60405160208183030381529060405280519060200120613bfa565b8284606001613c44565b73__$1f7cbacb1f9f5d323b85b0487838426c8d$__636a7ecf1383356114736040860160208701614b22565b61148060408701876154a5565b87356000908152601360205260409081902090516001600160e01b031960e088901b1681526114b9959493929190600e90600401615514565b60006040518083038186803b1580156114d157600080fd5b505af4158015610e0a573d6000803e3d6000fd5b60006114ef613b6b565b60006114fb83356119c0565b905061163661162c7ffb2be94758c5f2515e99eae2d54860ea468ce314ffb38e328c234793acbcccf4853561153360208801886154a5565b6040516115419291906153a9565b604051809103902087604001602081019061155c9190614b22565b61156960608a018a6154a5565b6040516115779291906153a9565b60405190819003902061159060a08b0160808c01614b22565b61159d60a08c018c6154a5565b6040516115ab9291906153a9565b604080519182900382206001600160a01b03808d166000908152600a6020908152908490208054600181019091559085019a909a52918301979097526060820195909552928416608084015260a083019190915290911660c082015260e0810191909152610100810191909152610120808701359082015261014001611422565b828560c001613c44565b611726833561164860208601866154a5565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061168d925050506060870160408801614b22565b61169a60608801886154a5565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506116df9250505060a0890160808a01614b22565b6116ec60a08a018a6154a5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250613d2292505050565b9392505050565b6117356138a2565b6001600160a01b038216600081815260106020908152604091829020805460ff191685151590811790915591514281529192917f37872a053ef20cb52defb7c9ec20e1a87cb8dd5098ac9e76a144be263dfef572910161101c565b610ce183838360405180602001604052806000815250612937565b6117b36135f2565b6117bc81613dc1565b6117c581613df1565b50565b6000818152600260205260408120546001600160a01b03161515610cbf565b60006117f260085490565b82106118555760405162461bcd60e51b815260206004820152602c60248201527f455243373231456e756d657261626c653a20676c6f62616c20696e646578206f60448201526b7574206f6620626f756e647360a01b6064820152608401610e87565b6008828154811061186857611868615554565b90600052602060002001549050919050565b600081815260026020526040812054600160a01b90046001600160601b0316806119015760405162461bcd60e51b815260206004820152603260248201527f4552433732313a206d696e742074696d657374616d7020717565727920666f72604482015271103737b732bc34b9ba32b73a103a37b5b2b760711b6064820152608401610e87565b6001600160601b031692915050565b6018546001600160a01b031633141561196357600081600281111561193757611937614af2565b141561195657604051630357a1ab60e31b815260040160405180910390fd5b61195e6135f2565b61198e565b6017546001600160a01b0316331461198e57604051635010559960e11b815260040160405180910390fd5b6117c58161375d565b6000828152601460209081526040808320848452909152902080546001909101545b9250929050565b6000818152600260205260408120546001600160a01b031680610cbf5760405162461bcd60e51b815260206004820152602960248201527f4552433732313a206f776e657220717565727920666f72206e6f6e657869737460448201526832b73a103a37b5b2b760b91b6064820152608401610e87565b611a3f6135f2565b611a4884613e2c565b600084815260136020526040908190209051636a7ecf1360e01b815273__$1f7cbacb1f9f5d323b85b0487838426c8d$__91636a7ecf1391611a9891889188918891889190600e90600401615514565b60006040518083038186803b158015611ab057600080fd5b505af4158015611ac4573d6000803e3d6000fd5b5050505050505050565b60006001600160a01b038216611b395760405162461bcd60e51b815260206004820152602a60248201527f4552433732313a2062616c616e636520717565727920666f7220746865207a65604482015269726f206164647265737360b01b6064820152608401610e87565b506001600160a01b031660009081526003602052604090205490565b6000611b5f613b6b565b6000611b6b83356119c0565b9050611d14611d097f35b861ea91c543d61c3f094b5effa29dfcd22a439290ae248fdccbc7162016918535611ba360208801886154a5565b604051611bb19291906153a9565b604051809103902087604001358860600135898060800190611bd391906154a5565b604051611be19291906153a9565b604051908190039020611bfa60c08c0160a08d01614b22565b611c0760c08d018d6154a5565b604051611c159291906153a9565b604051908190039020611c2f6101008e0160e08f01614b22565b611c3d6101008f018f6154a5565b604051611c4b9291906153a9565b6040518091039020600a60008e6001600160a01b03166001600160a01b031681526020019081526020016000206000815480929190600101919050558f61012001606001356040516020016114229c9b9a999897969594939291909b8c5260208c019a909a5260408b019890985260608a0196909652608089019490945260a08801929092526001600160a01b0390811660c088015260e0870191909152166101008501526101208401526101408301526101608201526101800190565b828561012001613c44565b61172660405180610120016040528085600001358152602001858060200190611d3d91906154a5565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050509082525060408087013560208301526060808801359183019190915201611d9a60808701876154a5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250505090825250602001611de460c0870160a08801614b22565b6001600160a01b03168152602001611dff60c08701876154a5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250505090825250602001611e4a610100870160e08801614b22565b6001600160a01b03168152602001611e666101008701876154a5565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505050915250613e66565b6001600160a01b038316611ec8576040516307eb16dd60e21b815260040160405180910390fd5b6000611ed3836119c0565b6001600160a01b038181166000908152600a602090815260409182902080546001810190915582517f49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad9281019290925292881691810191909152606080820187905260808201929092529084013560a0820152909150611f6090611f599060c001611422565b8284613c44565b611f6a8484613834565b50505050565b6000611f7a6135f2565b60405163334ce6f160e21b815273__$1e68a60ae0444699fe08192a29ecc09930$__9063cd339bc490611fe190339089908990899089907f00000000000000000000000000000000000000000000000000000000000000009060149060139060040161556a565b602060405180830381865af4158015611ffe573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061202291906155b8565b90505b949350505050565b60008581526014602090815260408083208784529091529020600501546001600160a01b03163381146120735760405163c6d1651b60e01b815260040160405180910390fd5b604080516001600160a01b03858116825284166020820152428183015290518591879189917f68edb7ec2c37d21b3b72233960b487f2966f4ac82b7430d39f24d1f8d6f99106919081900360600190a4505050505050565b6001600160a01b0383166120f2576040516307eb16dd60e21b815260040160405180910390fd5b6001600160a01b038481166000818152600a602090815260409182902080546001810190915582517f47ab88482c90e4bb94b82a947ae78fa91fb25de1469ab491f4c15b9a0a2677ee9281019290925291810192909252918516606080830191909152841515608083015260a08201929092529082013560c08201526121859061217e9060e001611422565b8583613c44565b611f6a848484613ec5565b606061219a6135f2565b60006121a960408401846155d1565b905090506000816001600160401b038111156121c7576121c7614f9d565b6040519080825280602002602001820160405280156121f0578160200160208202803683370190505b50905060005b828110156122685761220b60408601866155d1565b8281811061221b5761221b615554565b905060200281019061222d91906154a5565b60405161223b9291906153a9565b604051809103902082828151811061225557612255615554565b60209081029190910101526001016121f6565b5061236a6123547ffb6b7f1cd1b38daf3822aff0abbe78124db5d62a4748bcff007c15ccd6d30bc561229d60208801886155d1565b6040516020016122ae92919061561a565b60405160208183030381529060405280519060200120846040516020016122d59190615646565b60405160208183030381529060405280519060200120600a60008a60000160208101906123029190614b22565b6001600160a01b0316815260208082019290925260409081016000208054600181019091558151928301959095528101929092526060820152608081019190915260c08781013560a083015201611422565b6123616020870187614b22565b86606001613c44565b73__$1e68a60ae0444699fe08192a29ecc09930$__636fb4b4636123916020870187614b22565b61239e60208801886155d1565b6123ab60408a018a6155d1565b601360126040518863ffffffff1660e01b81526004016123d197969594939291906156c1565b600060405180830381865af41580156123ee573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052612025919081019061577b565b606060018054610cf590615374565b600061242f613b6b565b612439823561362c565b610cbf823561244b60208501856154a5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250612490925050506060860160408701614b22565b61249d60608701876154a5565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506124e29250505060a0880160808901614b22565b6116ec60a08901896154a5565b6124f76135f2565b600061250382356119c0565b90506125b06125a67f5b9860bd835e648945b22d053515bc1f53b7d9fab4b23b1b49db15722e945d14843561253b60208701876154a5565b6040516125499291906153a9565b604080519182900382206001600160a01b0388166000908152600a60209081529083902080546001810190915590840195909552908201929092526060810191909152608081019190915260a0858101359082015260c001611422565b8284604001613c44565b6125c782356125c260208501856154a5565b61368b565b5050565b6001600160a01b0382163314156126245760405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c6572000000000000006044820152606401610e87565b6125c7338383613ec5565b6126376138a2565b6001600160a01b0382166000818152600e6020908152604091829020805460ff191685151590811790915591514281529192917f52c5b7889df9f12f84ec3da051e854e5876678370d8357959c23ef59dd6486df910161101c565b61269a6138a2565b6117c5816137db565b60006126ad6135f2565b61278361276d7f7f9b4ea1fc678b4fda1611ac5cbd28f339e235d89b1540635e9b2e0223a3c101602085013560408601356126eb60608801886154a5565b6040516126f99291906153a9565b604051908190039020600a600061271360208b018b614b22565b6001600160a01b0316815260208082019290925260409081016000208054600181019091558151928301969096528101939093526060830191909152608082015260a081019190915260e08581013560c083015201611422565b61277a6020850185614b22565b84608001613c44565b73__$1e68a60ae0444699fe08192a29ecc09930$__63cd339bc46127aa6020850185614b22565b602085013560408601356127c160608801886154a5565b7f0000000000000000000000000000000000000000000000000000000000000000601460136040518963ffffffff1660e01b815260040161280998979695949392919061556a565b602060405180830381865af4158015612826573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cbf91906155b8565b606060008061285b85856014613f32565b506000828152601460209081526040808320848452909152902060020180549294509092509061288a90615374565b80601f01602080910402602001604051908101604052809291908181526020018280546128b690615374565b80156129035780601f106128d857610100808354040283529160200191612903565b820191906000526020600020905b8154815290600101906020018083116128e657829003601f168201915b50505050509250505092915050565b600061291c613b6b565b612926823561362c565b610cbf61293283615820565b613e66565b61294133836138cd565b61295d5760405162461bcd60e51b8152600401610e87906153b9565b611f6a84848484613ff4565b6129716135f2565b600061297d82356119c0565b90506129b56125a67fd8d76e8b2b26e1ebe72def13fb559a68561ef064055b0de01f955bc26e25d42f843561253b60208701876154a5565b6125c782356129c760208501856154a5565b614027565b6000848152601360205260409020600201546001600160a01b0316338114612a075760405163646785d560e11b815260040160405180910390fd5b604080516001600160a01b0385811682528416602082015242818301529051859187917f4996ad2257e7db44908136c43128cc10ca988096f67dc6bb0bcee11d151368fb9181900360600190a35050505050565b612a636135f2565b6000612a6f82356119c0565b9050612b006125a67f77ba3e9f5fa75343bbad1241fb539a0064de97694b47d463d1eb5c54aba11f0f8435612aaa6040870160208801614b22565b6001600160a01b038681166000908152600a602090815260409182902080546001810190915582519182019690965290810193909352166060820152608081019190915260a0858101359082015260c001611422565b6125c78235612b156040850160208601614b22565b614078565b612b226135f2565b612b2b82613e2c565b6125c78282614078565b60408051808201909152600080825260208201526000828152600260205260409020546001600160a01b0316612bc45760405162461bcd60e51b815260206004820152602e60248201527f4552433732313a20746f6b656e206461746120717565727920666f72206e6f6e60448201526d32bc34b9ba32b73a103a37b5b2b760911b6064820152608401610e87565b506000908152600260209081526040918290208251808401909352546001600160a01b0381168352600160a01b90046001600160601b03169082015290565b612c0b6135f2565b612c148361362c565b610ce1838383614027565b612c736040518060c0016040528060008152602001600081526020016060815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b031681525090565b6014600084815260200190815260200160002060008381526020019081526020016000206040518060c00160405290816000820154815260200160018201548152602001600282018054612cc690615374565b80601f0160208091040260200160405190810160405280929190818152602001828054612cf290615374565b8015612d3f5780601f10612d1457610100808354040283529160200191612d3f565b820191906000526020600020905b815481529060010190602001808311612d2257829003601f168201915b505050918352505060038201546001600160a01b0390811660208301526004830154811660408301526005909201549091166060909101529392505050565b6000818152601360205260409020600201546060906001600160a01b031673__$f906f20d797116ee89ed79945048c6ad36$__633edea73c848315612e2457836001600160a01b03166318160ddd6040518163ffffffff1660e01b8152600401602060405180830381865afa158015612dfb573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612e1f91906155b8565b612e27565b60005b612e30876119c0565b6000888152601360205260409081902090516001600160e01b031960e087901b168152612e6b94939291600381019160049182019101615993565b600060405180830381865af4158015612e88573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261172691908101906159d2565b612eb86135f2565b612f79612f637e198a25e19615d82f53977db0fc1ee7f3701b0fdf1299a971cf997545879c04612eeb6020850185614b22565b60208501803590600a90600090612f029089614b22565b6001600160a01b03168152602080820192909252604090810160002080546001810190915590516114229594939260a08a013591019485526001600160a01b0393909316602085015260408401919091526060830152608082015260a00190565b612f706020840184614b22565b83604001613c44565b6117c5612f896020830183614b22565b82602001356140d5565b612f9b6135f2565b612fa5828261416d565b6125c782613df1565b6000612fb8613b6b565b6000612fc483356119c0565b90506130c961162c7f4178441487eb967c15821c9ffee94853812f263a69ccb6352bdaac59f77e486f85356020870135604088013561300660608a018a6154a5565b6040516130149291906153a9565b60405190819003902061302d60a08b0160808c01614b22565b61303a60a08c018c6154a5565b6040516130489291906153a9565b604080519182900382206001600160a01b03808d166000908152600a6020908152908490208054600181019091559085019a909a52918301979097526060820195909552608081019390935260a083019190915290911660c082015260e0810191909152610100810191909152610120808701359082015261014001611422565b6117266040518060c0016040528085600001358152602001856020013581526020018560400135815260200185806060019061310591906154a5565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050509082525060200161314f60a0870160808801614b22565b6001600160a01b0316815260200161316a60a08701876154a5565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505050915250613ba2565b60008181526013602052604090206003018054606091906112c490615374565b60006131cf6141fb565b905090565b61321f6040518060c001604052806000815260200160006001600160a01b0316815260200160006001600160a01b031681526020016060815260200160608152602001606081525090565b600082815260136020908152604091829020825160c0810184528154815260018201546001600160a01b039081169382019390935260028201549092169282019290925260038201805491929160608401919061327b90615374565b80601f01602080910402602001604051908101604052809291908181526020018280546132a790615374565b80156132f45780601f106132c9576101008083540402835291602001916132f4565b820191906000526020600020905b8154815290600101906020018083116132d757829003601f168201915b5050505050815260200160048201805461330d90615374565b80601f016020809104026020016040519081016040528092919081815260200182805461333990615374565b80156133865780601f1061335b57610100808354040283529160200191613386565b820191906000526020600020905b81548152906001019060200180831161336957829003601f168201915b5050505050815260200160058201805461339f90615374565b80601f01602080910402602001604051908101604052809291908181526020018280546133cb90615374565b80156134185780601f106133ed57610100808354040283529160200191613418565b820191906000526020600020905b8154815290600101906020018083116133fb57829003601f168201915b5050505050815250509050919050565b6134306135f2565b6117c533826140d5565b60606134446135f2565b604051636fb4b46360e01b815273__$1e68a60ae0444699fe08192a29ecc09930$__90636fb4b4639061348990339089908990899089906013906012906004016156c1565b600060405180830381865af41580156134a6573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052612022919081019061577b565b60006134d86135f2565b336000908152600d602052604090205460ff166135085760405163561a858760e01b815260040160405180910390fd5b60168054600101908190556135296135236020850185614b22565b82614290565b604051633f67a66f60e11b815273__$1f7cbacb1f9f5d323b85b0487838426c8d$__90637ecf4cde9061356b9086908590601290601390600e90600401615a3f565b60006040518083038186803b15801561358357600080fd5b505af4158015613597573d6000803e3d6000fd5b509295945050505050565b60006001600160e01b031982166380ac58cd60e01b14806135d357506001600160e01b03198216635b5e139f60e01b145b80610cbf57506301ffc9a760e01b6001600160e01b0319831614610cbf565b6002600c5460ff16600281111561360b5761360b614af2565b141561362a576040516313d0ff5960e31b815260040160405180910390fd5b565b613635816119c0565b6001600160a01b0316336001600160a01b0316148061366a57506000818152601160205260409020546001600160a01b031633145b156136725750565b60405163f33ac98f60e01b815260040160405180910390fd5b6117708111156136ae5760405163781548c760e01b815260040160405180910390fd5b60008381526013602052604090206136ca906004018383614855565b50827fd5a5879cad33c830cc1432c1850107029a09c80c60e9bce1ecd08d24880bd46c8383426040516136ff93929190615b36565b60405180910390a2505050565b613718848484846143e1565b7f414cd0b34676984f09a5f76ce9718d4062e50283abe0e7e274a9a5b4e0c99c30848484844260405161374f959493929190615b5a565b60405180910390a150505050565b600c805460ff811691839160ff1916600183600281111561378057613780614af2565b021790555081600281111561379757613797614af2565b8160028111156137a9576137a9614af2565b60405142815233907fa2f9a1499fc1f9b7796d21fe5761290ccb7e0ef6ccf35fa58b668f304a62a1ca90602001611298565b601780546001600160a01b038381166001600160a01b03198316811790935560405191169190829033907fe552a55455b740845a5c07ed445d1724142fc997b389835495a29b30cddc1ccd906112989042815260200190565b600081815260046020526040902080546001600160a01b0319166001600160a01b0384169081179091558190613869826119c0565b6001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b6017546001600160a01b0316331461362a57604051632d5be4cb60e21b815260040160405180910390fd5b6000818152600260205260408120546001600160a01b03166139465760405162461bcd60e51b815260206004820152602c60248201527f4552433732313a206f70657261746f7220717565727920666f72206e6f6e657860448201526b34b9ba32b73a103a37b5b2b760a11b6064820152608401610e87565b6000613951836119c0565b9050806001600160a01b0316846001600160a01b0316148061398c5750836001600160a01b031661398184610e12565b6001600160a01b0316145b8061202557506001600160a01b0380821660009081526005602090815260408083209388168352929052205460ff16612025565b826001600160a01b03166139d3826119c0565b6001600160a01b031614613a3b5760405162461bcd60e51b815260206004820152602960248201527f4552433732313a207472616e73666572206f6620746f6b656e2074686174206960448201526839903737ba1037bbb760b91b6064820152608401610e87565b6001600160a01b038216613a9d5760405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f206164646044820152637265737360e01b6064820152608401610e87565b613aa8838383614401565b613ab3600082613834565b6001600160a01b0383166000908152600360205260408120805460019290613adc908490615baa565b90915550506001600160a01b0382166000908152600360205260408120805460019290613b0a908490615bc1565b909155505060008181526002602052604080822080546001600160a01b0319166001600160a01b0386811691821790925591518493918716917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91a4505050565b6000600c5460ff166002811115613b8457613b84614af2565b1461362a57604051630f392a3b60e31b815260040160405180910390fd5b8051600090815260136020526040808220805460010190819055905163764e3e9360e01b815273__$1f7cbacb1f9f5d323b85b0487838426c8d$__9063764e3e939061356b9086908590601490601090600401615bd9565b600080613c056141fb565b60405161190160f01b602082015260228101919091526042810184905260620160408051601f1981840301815291905280516020909101209392505050565b4281606001351015613c6957604051630819bdcd60e01b815260040160405180910390fd5b6000600184613c7b6020850185615c65565b604080516000815260208181018084529490945260ff9092168282015291850135606082015290840135608082015260a0016020604051602081039080840390855afa158015613ccf573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b0381161580613d045750826001600160a01b0316816001600160a01b031614155b15611f6a576040516337e8456b60e01b815260040160405180910390fd5b60008681526013602052604080822080546001019081905590516309dbf05160e41b815273__$1f7cbacb1f9f5d323b85b0487838426c8d$__90639dbf051090613d85908b908b908b908b908b908b908a90601490600f90601090600401615c88565b60006040518083038186803b158015613d9d57600080fd5b505af4158015613db1573d6000803e3d6000fd5b50929a9950505050505050505050565b613dcb33826138cd565b613de857604051636d8a29e760e11b815260040160405180910390fd5b6117c581614476565b6000818152601360205260408082209051613e0f9160030190615d0f565b604080519182900390912060009081526012602052908120555050565b613e35816119c0565b6001600160a01b0316336001600160a01b0316146117c55760405163f194fae560e01b815260040160405180910390fd5b80516000908152601360208190526040808320805460010190819055905163731d549f60e11b8152909173__$1f7cbacb1f9f5d323b85b0487838426c8d$__9163e63aa93e9161356b9187918691601490600f90601090600401615d7d565b6001600160a01b03838116600081815260056020908152604080832094871680845294825291829020805460ff191686151590811790915591519182527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31910160405180910390a3505050565b600083815260208281526040808320858452909152812060040154819081906001600160a01b03168015613f6d578693508592509050613feb565b60008781526020868152604080832089845290915290205480613fa35760405163a43d2a7160e01b815260040160405180910390fd5b6000888152602087815260408083208a84528252808320600101548484528983528184208185529092529091206004015491955093506001600160a01b03169150613feb9050565b93509350939050565b613fff8484846139c0565b61400b84848484614512565b611f6a5760405162461bcd60e51b8152600401610e8790615e7f565b6000838152601360205260409020614043906005018383614855565b50827fe82886e1af6fcab5caef13815b22f51384e970c367a785f265d13860a7d6966d8383426040516136ff93929190615b36565b60008281526011602090815260409182902080546001600160a01b0319166001600160a01b038516908117909155915142815284917f22baaec4952f35f59e45bd2ddb287e1ccc6d319375770c09428eb8f8d604e065910161101c565b6000811180156140ff57506140e9816119c0565b6001600160a01b0316826001600160a01b031614155b1561411d5760405163f194fae560e01b815260040160405180910390fd5b6001600160a01b03821660008181526015602052604090819020839055518291907f0afd7c479e8bc7dcdb856b3cc27d2332dfe1f018fde574ea124919ddcae8a9339061101c9042815260200190565b6000614178836119c0565b6001600160a01b0381166000908152600a602090815260409182902080546001810190915582517f108ccda6d7331b00561a3eea66a2ae331622356585681c62731e4a01aae2261a928101929092529181018690526060808201929092529084013560808201529091506141f290611f599060a001611422565b610ce183614476565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f614226610ce6565b80516020918201206040805192830193909352918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b6001600160a01b0382166142e65760405162461bcd60e51b815260206004820181905260248201527f4552433732313a206d696e7420746f20746865207a65726f20616464726573736044820152606401610e87565b6000818152600260205260409020546001600160a01b03161561434b5760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e746564000000006044820152606401610e87565b61435760008383614401565b6001600160a01b0382166000908152600360205260408120805460019290614380908490615bc1565b90915550506000818152600260205260408082206001600160a01b038516600160a01b426001600160601b031602811790915590518392907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908290a45050565b6143ed60008585614855565b506143fa60018383614855565b5050505050565b6144096135f2565b6000818152601160205260409020546001600160a01b03161561443157614431816000614078565b6001600160a01b03831660009081526015602052604090205481141561446b576001600160a01b0383166000908152601560205260408120555b610ce183838361460d565b6000614481826119c0565b905061448f81600084614401565b61449a600083613834565b6001600160a01b03811660009081526003602052604081208054600192906144c3908490615baa565b9091555050600082815260026020526040808220829055518391906001600160a01b038416907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908390a45050565b60006001600160a01b0384163b1561460557604051630a85bd0160e11b81526001600160a01b0385169063150b7a0290614556903390899088908890600401615ed1565b6020604051808303816000875af1925050508015614591575060408051601f3d908101601f1916820190925261458e91810190615f0e565b60015b6145eb573d8080156145bf576040519150601f19603f3d011682016040523d82523d6000602084013e6145c4565b606091505b5080516145e35760405162461bcd60e51b8152600401610e8790615e7f565b805181602001fd5b6001600160e01b031916630a85bd0160e11b149050612025565b506001612025565b6001600160a01b0383166146685761466381600880546000838152600960205260408120829055600182018355919091527ff3f7a9fe364faab93b216da50a3214154f22a0a2b415b23a84c8169e8b636ee30155565b61468b565b816001600160a01b0316836001600160a01b03161461468b5761468b83826146c5565b6001600160a01b0382166146a257610ce181614762565b826001600160a01b0316826001600160a01b031614610ce157610ce18282614811565b600060016146d284611ace565b6146dc9190615baa565b60008381526007602052604090205490915080821461472f576001600160a01b03841660009081526006602090815260408083208584528252808320548484528184208190558352600790915290208190555b5060009182526007602090815260408084208490556001600160a01b039094168352600681528383209183525290812055565b60085460009061477490600190615baa565b6000838152600960205260408120546008805493945090928490811061479c5761479c615554565b9060005260206000200154905080600883815481106147bd576147bd615554565b60009182526020808320909101929092558281526009909152604080822084905585825281205560088054806147f5576147f5615f2b565b6001900381819060005260206000200160009055905550505050565b600061481c83611ace565b6001600160a01b039093166000908152600660209081526040808320868452825280832085905593825260079052919091209190915550565b82805461486190615374565b90600052602060002090601f01602090048101928261488357600085556148c9565b82601f1061489c5782800160ff198235161785556148c9565b828001600101855582156148c9579182015b828111156148c95782358255916020019190600101906148ae565b506148d59291506148d9565b5090565b5b808211156148d557600081556001016148da565b6001600160e01b0319811681146117c557600080fd5b60006020828403121561491657600080fd5b8135611726816148ee565b60008083601f84011261493357600080fd5b5081356001600160401b0381111561494a57600080fd5b6020830191508360208285010111156119b957600080fd5b60008060006040848603121561497757600080fd5b8335925060208401356001600160401b0381111561499457600080fd5b6149a086828701614921565b9497909650939450505050565b60005b838110156149c85781810151838201526020016149b0565b83811115611f6a5750506000910152565b600081518084526149f18160208601602086016149ad565b601f01601f19169290920160200192915050565b60208152600061172660208301846149d9565b80356001600160a01b038116811461114657600080fd5b600080600080600060608688031215614a4757600080fd5b85356001600160401b0380821115614a5e57600080fd5b614a6a89838a01614921565b90975095506020880135915080821115614a8357600080fd5b50614a9088828901614921565b9094509250614aa3905060408701614a18565b90509295509295909350565b600060208284031215614ac157600080fd5b5035919050565b60008060408385031215614adb57600080fd5b614ae483614a18565b946020939093013593505050565b634e487b7160e01b600052602160045260246000fd5b6020810160038310614b1c57614b1c614af2565b91905290565b600060208284031215614b3457600080fd5b61172682614a18565b8035801515811461114657600080fd5b60008060408385031215614b6057600080fd5b614b6983614a18565b9150614b7760208401614b3d565b90509250929050565b60008060208385031215614b9357600080fd5b82356001600160401b03811115614ba957600080fd5b614bb585828601614921565b90969095509350505050565b600080600060608486031215614bd657600080fd5b614bdf84614a18565b9250614bed60208501614a18565b9150604084013590509250925092565b600060c08284031215614c0f57600080fd5b50919050565b600060208284031215614c2757600080fd5b81356001600160401b03811115614c3d57600080fd5b61202584828501614bfd565b60008060408385031215614c5c57600080fd5b50508035926020909101359150565b6020810160048310614b1c57614b1c614af2565b600060e08284031215614c0f57600080fd5b600060208284031215614ca357600080fd5b81356001600160401b03811115614cb957600080fd5b61202584828501614c7f565b60006101408284031215614c0f57600080fd5b600060208284031215614cea57600080fd5b81356001600160401b03811115614d0057600080fd5b61202584828501614cc5565b600060208284031215614d1e57600080fd5b81356003811061172657600080fd5b60008060008060608587031215614d4357600080fd5b84359350614d5360208601614a18565b925060408501356001600160401b03811115614d6e57600080fd5b614d7a87828801614921565b95989497509550505050565b600060208284031215614d9857600080fd5b81356001600160401b03811115614dae57600080fd5b82016101a0818503121561172657600080fd5b600060808284031215614c0f57600080fd5b600080600060c08486031215614de857600080fd5b614df184614a18565b925060208401359150614e078560408601614dc1565b90509250925092565b60008060008060608587031215614e2657600080fd5b843593506020850135925060408501356001600160401b03811115614d6e57600080fd5b600080600080600060a08688031215614e6257600080fd5b853594506020860135935060408601359250614e8060608701614a18565b9150614aa360808701614a18565b60008060008060e08587031215614ea457600080fd5b614ead85614a18565b9350614ebb60208601614a18565b9250614ec960408601614b3d565b9150614ed88660608701614dc1565b905092959194509250565b6020808252825182820181905260009190848201906040850190845b81811015614f1b57835183529284019291840191600101614eff565b50909695505050505050565b600060208284031215614f3957600080fd5b81356001600160401b03811115614f4f57600080fd5b8201610100818503121561172657600080fd5b600060208284031215614f7457600080fd5b81356001600160401b03811115614f8a57600080fd5b8201610120818503121561172657600080fd5b634e487b7160e01b600052604160045260246000fd5b60405160c081016001600160401b0381118282101715614fd557614fd5614f9d565b60405290565b60405161012081016001600160401b0381118282101715614fd557614fd5614f9d565b604051601f8201601f191681016001600160401b038111828210171561502657615026614f9d565b604052919050565b60006001600160401b0382111561504757615047614f9d565b50601f01601f191660200190565b600082601f83011261506657600080fd5b81356150796150748261502e565b614ffe565b81815284602083860101111561508e57600080fd5b816020850160208301376000918101602001919091529392505050565b600080600080608085870312156150c157600080fd5b6150ca85614a18565b93506150d860208601614a18565b92506040850135915060608501356001600160401b038111156150fa57600080fd5b61510687828801615055565b91505092959194509250565b6000806000806080858703121561512857600080fd5b843593506020850135925061513f60408601614a18565b9150614ed860608601614a18565b600060c0828403121561515f57600080fd5b6117268383614bfd565b6000806040838503121561517c57600080fd5b82359150614b7760208401614a18565b6020815281516020820152602082015160408201526000604083015160c060608401526151bc60e08401826149d9565b9050606084015160018060a01b0380821660808601528060808701511660a08601528060a08701511660c086015250508091505092915050565b60008060a0838503121561520957600080fd5b82359150614b778460208501614dc1565b6000806040838503121561522d57600080fd5b61523683614a18565b9150614b7760208401614a18565b60208152815160208201526000602083015160018060a01b0380821660408501528060408601511660608501525050606083015160c0608084015261528c60e08401826149d9565b90506080840151601f19808584030160a08601526152aa83836149d9565b925060a08601519150808584030160c0860152506152c882826149d9565b95945050505050565b60008083601f8401126152e357600080fd5b5081356001600160401b038111156152fa57600080fd5b6020830191508360208260051b85010111156119b957600080fd5b6000806000806040858703121561532b57600080fd5b84356001600160401b038082111561534257600080fd5b61534e888389016152d1565b9096509450602087013591508082111561536757600080fd5b50614d7a878288016152d1565b600181811c9082168061538857607f821691505b60208210811415614c0f57634e487b7160e01b600052602260045260246000fd5b8183823760009101908152919050565b60208082526031908201527f4552433732313a207472616e736665722063616c6c6572206973206e6f74206f6040820152701ddb995c881b9bdc88185c1c1c9bdd9959607a1b606082015260800190565b600060c0823603121561541c57600080fd5b615424614fb3565b82358152602083013560208201526040830135604082015260608301356001600160401b038082111561545657600080fd5b61546236838701615055565b606084015261547360808601614a18565b608084015260a085013591508082111561548c57600080fd5b5061549936828601615055565b60a08301525092915050565b6000808335601e198436030181126154bc57600080fd5b8301803591506001600160401b038211156154d657600080fd5b6020019150368190038213156119b957600080fd5b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b8681526001600160a01b038616602082015260a06040820181905260009061553f90830186886154eb565b60608301949094525060800152949350505050565b634e487b7160e01b600052603260045260246000fd5b600060018060a01b03808b16835289602084015288604084015260e0606084015261559960e08401888a6154eb565b951660808301525060a081019290925260c09091015295945050505050565b6000602082840312156155ca57600080fd5b5051919050565b6000808335601e198436030181126155e857600080fd5b8301803591506001600160401b0382111561560257600080fd5b6020019150600581901b36038213156119b957600080fd5b60006001600160fb1b0383111561563057600080fd5b8260051b80858437600092019182525092915050565b815160009082906020808601845b8381101561567057815185529382019390820190600101615654565b50929695505050505050565b6000808335601e1984360301811261569357600080fd5b83016020810192503590506001600160401b038111156156b257600080fd5b8036038313156119b957600080fd5b6001600160a01b038816815260a060208083018290529082018790526000906001600160fb1b038811156156f457600080fd5b8760051b808a60c0860137830183810360c090810160408601528101879052600587901b810160e09081019082018960005b8a8110156157605784840360df19018352615741828d61567c565b61574c8682846154eb565b955050509185019190850190600101615726565b50505060608501969096525050506080015295945050505050565b6000602080838503121561578e57600080fd5b82516001600160401b03808211156157a557600080fd5b818501915085601f8301126157b957600080fd5b8151818111156157cb576157cb614f9d565b8060051b91506157dc848301614ffe565b81815291830184019184810190888411156157f657600080fd5b938501935b83851015615814578451825293850193908501906157fb565b98975050505050505050565b6000610120823603121561583357600080fd5b61583b614fdb565b8235815260208301356001600160401b038082111561585957600080fd5b61586536838701615055565b60208401526040850135604084015260608501356060840152608085013591508082111561589257600080fd5b61589e36838701615055565b60808401526158af60a08601614a18565b60a084015260c08501359150808211156158c857600080fd5b6158d436838701615055565b60c08401526158e560e08601614a18565b60e08401526101009150818501358181111561590057600080fd5b61590c36828801615055565b8385015250505080915050919050565b6000815461592981615374565b808552602060018381168015615946576001811461595a57615988565b60ff19851688840152604088019550615988565b866000528260002060005b858110156159805781548a8201860152908301908401615965565b890184019650505b505050505092915050565b85815284602082015260018060a01b038416604082015260a0606082015260006159c060a083018561591c565b8281036080840152615814818561591c565b6000602082840312156159e457600080fd5b81516001600160401b038111156159fa57600080fd5b8201601f81018413615a0b57600080fd5b8051615a196150748261502e565b818152856020838501011115615a2e57600080fd5b6152c88260208301602086016149ad565b60a0815260006001600160a01b0380615a5789614a18565b1660a0840152615a6a602089018961567c565b60c080860152615a7f610160860182846154eb565b915050615a8f60408a018a61567c565b609f19808785030160e0880152615aa78483856154eb565b935084615ab660608e01614a18565b16610100880152615aca60808d018d61567c565b955092508087850301610120880152615ae48486856154eb565b9450615af360a08d018d61567c565b9450925080878603016101408801525050615b0f8383836154eb565b93505050508560208301528460408301528360608301528260808301529695505050505050565b604081526000615b4a6040830185876154eb565b9050826020830152949350505050565b606081526000615b6e6060830187896154eb565b8281036020840152615b818186886154eb565b9150508260408301529695505050505050565b634e487b7160e01b600052601160045260246000fd5b600082821015615bbc57615bbc615b94565b500390565b60008219821115615bd457615bd4615b94565b500190565b6080815284516080820152602085015160a0820152604085015160c08201526000606086015160c060e0840152615c146101408401826149d9565b60808801516001600160a01b031661010085015260a0880151848203607f1901610120860152909150615c4782826149d9565b60208501979097525050506040810192909252606090910152919050565b600060208284031215615c7757600080fd5b813560ff8116811461172657600080fd5b60006101408c8352806020840152615ca28184018d6149d9565b6001600160a01b038c811660408601528482036060860152909150615cc7828c6149d9565b908a16608085015283810360a08501529050615ce381896149d9565b9150508560c08301528460e083015283610100830152826101208301529b9a5050505050505050505050565b6000808354615d1d81615374565b60018281168015615d355760018114615d4657615670565b60ff19841687528287019450615670565b8760005260208060002060005b85811015615d6c5781548a820152908401908201615d53565b505050959091019695505050505050565b60c08152865160c0820152600060208801516101208060e0850152615da66101e08501836149d9565b915060408a0151610100818187015260608c01518387015260808c0151925060bf1991508186850301610140870152615ddf84846149d9565b935060a08c01519250615dfe6101608701846001600160a01b03169052565b60c08c015192508186850301610180870152615e1a84846149d9565b935060e08c01519250615e396101a08701846001600160a01b03169052565b8b01518584039091016101c08601529050615e5482826149d9565b602085019990995250505060408101949094526060840192909252608083015260a090910152919050565b60208082526032908201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560408201527131b2b4bb32b91034b6b83632b6b2b73a32b960711b606082015260800190565b6001600160a01b0385811682528416602082015260408101839052608060608201819052600090615f04908301846149d9565b9695505050505050565b600060208284031215615f2057600080fd5b8151611726816148ee565b634e487b7160e01b600052603160045260246000fdfea2646970667358221220b22320e1e51caa663be0e995a3451f4dfdfa558ea5b06e71a5454229c1bc455b64736f6c634300080a0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106104805760003560e01c806384114ad411610257578063bd12d3f011610146578063dd69cdb1116100c3578063f08f4f6411610087578063f08f4f6414610c21578063f1b2f8bc14610c41578063f990ccd714610c54578063fb78ae6c14610c74578063ffea138e14610c8757600080fd5b8063dd69cdb114610ba4578063df457c3414610bb7578063e985e9c514610bca578063ec81d19414610c06578063ed24911d14610c1957600080fd5b8063c6b5d06c1161010a578063c6b5d06c14610b1f578063c736c99014610b32578063c87b56dd14610b52578063d923d20c14610b65578063dc21725314610b9157600080fd5b8063bd12d3f014610a93578063bdfdd4bc14610aa6578063bfbf0b4b14610ab9578063bfd24f4714610acc578063c0da9bcd14610adf57600080fd5b8063a6d8e1e4116101d4578063b48951e411610198578063b48951e414610a10578063b5a3149614610a23578063b6f32d2b14610a36578063b7984c0514610a49578063b88d4fde14610a8057600080fd5b8063a6d8e1e414610966578063a9ec656314610979578063ab033ea9146109a5578063ad3e72bf146109b8578063af05dd22146109e457600080fd5b806392254a621161021b57806392254a62146108fc57806395d89b4114610925578063963ff1411461092d5780639b84a14c14610940578063a22cb4651461095357600080fd5b806384114ad41461087757806386e2947b1461088a57806389028a131461089d5780638e204fb4146108b05780638e4fd6a9146108dc57600080fd5b80633a15ff0711610373578063540528b9116102f05780636dea40b3116102b45780636dea40b31461080557806370a082311461081857806377349a5f1461082b5780637a375716146108515780637ef67f991461086457600080fd5b8063540528b91461075757806356de96db1461078057806357ff49f6146107935780635ca3eebf146107ca5780636352211e146107f257600080fd5b806342966c681161033757806342966c68146106d45780634f558e79146106e75780634f6ccce7146106fa57806350ddf35c1461070d57806352aaef551461072057600080fd5b80633a15ff07146106685780633b28b89f146106885780633b5081321461069b5780634187e4c5146106ae57806342842e0e146106c157600080fd5b806320fa728a1161040157806331dcebe3116103c557806331dcebe3146105e957806331fff07c146105fc5780633502ac4b1461061c57806335da339414610642578063374c94731461065557600080fd5b806320fa728a1461058c57806323b872dd1461059f578063289b3c0d146105b25780632f745c59146105c35780632faeed81146105d657600080fd5b8063095ea7b311610448578063095ea7b31461051557806318160ddd146105285780631865c57d1461053a5780631cbb26201461054d578063209055061461057957600080fd5b806301ffc9a714610485578063054871b8146104ad57806306fdde03146104c2578063077f224a146104d7578063081812fc146104ea575b600080fd5b610498610493366004614904565b610c9a565b60405190151581526020015b60405180910390f35b6104c06104bb366004614962565b610cc5565b005b6104ca610ce6565b6040516104a49190614a05565b6104c06104e5366004614a2f565b610d78565b6104fd6104f8366004614aaf565b610e12565b6040516001600160a01b0390911681526020016104a4565b6104c0610523366004614ac8565b610eac565b6008545b6040519081526020016104a4565b600c5460ff166040516104a49190614b08565b61049861055b366004614b22565b6001600160a01b03166000908152600e602052604090205460ff1690565b6104c0610587366004614b4d565b610fbd565b61052c61059a366004614b80565b611028565b6104c06105ad366004614bc1565b61105a565b6017546001600160a01b03166104fd565b61052c6105d1366004614ac8565b61108b565b61052c6105e4366004614c15565b611121565b6104c06105f7366004614b4d565b61114b565b61060f61060a366004614c49565b6111ae565b6040516104a49190614c6b565b7f00000000000000000000000000000000000000000000000000000000000000006104fd565b6104c0610650366004614b22565b611237565b6104ca610663366004614aaf565b6112a4565b61052c610676366004614aaf565b60009081526013602052604090205490565b6104c0610696366004614c91565b611349565b61052c6106a9366004614cd8565b6114e5565b6104c06106bc366004614b4d565b61172d565b6104c06106cf366004614bc1565b611790565b6104c06106e2366004614aaf565b6117ab565b6104986106f5366004614aaf565b6117c8565b61052c610708366004614aaf565b6117e7565b61052c61071b366004614aaf565b61187a565b6104fd61072e366004614c49565b60009182526014602090815260408084209284529190529020600501546001600160a01b031690565b6104fd610765366004614aaf565b6000908152601160205260409020546001600160a01b031690565b6104c061078e366004614d0c565b611910565b6104fd6107a1366004614c49565b60009182526014602090815260408084209284529190529020600401546001600160a01b031690565b6107dd6107d8366004614c49565b611997565b604080519283526020830191909152016104a4565b6104fd610800366004614aaf565b6119c0565b6104c0610813366004614d2d565b611a37565b61052c610826366004614b22565b611ace565b7f00000000000000000000000000000000000000000000000000000000000000006104fd565b61052c61085f366004614d86565b611b55565b6104c0610872366004614dd3565b611ea1565b61052c610885366004614e10565b611f70565b6104c0610898366004614e4a565b61202d565b6104c06108ab366004614e8e565b6120cb565b6104986108be366004614b22565b6001600160a01b031660009081526010602052604090205460ff1690565b6108ef6108ea366004614c91565b612190565b6040516104a49190614ee3565b61052c61090a366004614b22565b6001600160a01b031660009081526015602052604090205490565b6104ca612416565b61052c61093b366004614c15565b612425565b6104c061094e366004614c15565b6124ef565b6104c0610961366004614b4d565b6125cb565b6104c0610974366004614b4d565b61262f565b6104fd610987366004614aaf565b6000908152601360205260409020600201546001600160a01b031690565b6104c06109b3366004614b22565b612692565b6104986109c6366004614b22565b6001600160a01b03166000908152600f602052604090205460ff1690565b6104986109f2366004614b22565b6001600160a01b03166000908152600d602052604090205460ff1690565b61052c610a1e366004614f27565b6126a3565b6104ca610a31366004614c49565b61284a565b61052c610a44366004614f62565b612912565b6104fd610a57366004614c49565b60009182526014602090815260408084209284529190529020600301546001600160a01b031690565b6104c0610a8e3660046150ab565b612937565b6104c0610aa1366004614c15565b612969565b6104c0610ab4366004615112565b6129cc565b6104c0610ac736600461514d565b612a5b565b6104c0610ada366004615169565b612b1a565b610af2610aed366004614aaf565b612b35565b6040805182516001600160a01b031681526020928301516001600160601b031692810192909252016104a4565b6104c0610b2d366004614962565b612c03565b610b45610b40366004614c49565b612c1f565b6040516104a4919061518c565b6104ca610b60366004614aaf565b612d7e565b6104fd610b73366004614aaf565b6000908152601360205260409020600101546001600160a01b031690565b6104c0610b9f36600461514d565b612eb0565b6104c0610bb23660046151f6565b612f93565b61052c610bc5366004614cd8565b612fae565b610498610bd836600461521a565b6001600160a01b03918216600090815260056020908152604080832093909416825291909152205460ff1690565b6104ca610c14366004614aaf565b6131a5565b61052c6131c5565b610c34610c2f366004614aaf565b6131d4565b6040516104a49190615244565b6104c0610c4f366004614aaf565b613428565b61052c610c62366004614b22565b600a6020526000908152604090205481565b6108ef610c82366004615315565b61343a565b61052c610c95366004614c15565b6134ce565b60006001600160e01b0319821663780e9d6360e01b1480610cbf5750610cbf826135a2565b92915050565b610ccd6135f2565b610cd68361362c565b610ce183838361368b565b505050565b606060008054610cf590615374565b80601f0160208091040260200160405190810160405280929190818152602001828054610d2190615374565b8015610d6e5780601f10610d4357610100808354040283529160200191610d6e565b820191906000526020600020905b815481529060010190602001808311610d5157829003601f168201915b5050505050905090565b60017f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316301415610dc4576040516325c7410560e21b815260040160405180910390fd5b600b548111610de6576040516302ed543d60e51b815260040160405180910390fd5b600b819055610df78686868661370c565b610e01600261375d565b610e0a826137db565b505050505050565b6000818152600260205260408120546001600160a01b0316610e905760405162461bcd60e51b815260206004820152602c60248201527f4552433732313a20617070726f76656420717565727920666f72206e6f6e657860448201526b34b9ba32b73a103a37b5b2b760a11b60648201526084015b60405180910390fd5b506000908152600460205260409020546001600160a01b031690565b6000610eb7826119c0565b9050806001600160a01b0316836001600160a01b03161415610f255760405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e656044820152603960f91b6064820152608401610e87565b336001600160a01b0382161480610f415750610f418133610bd8565b610fb35760405162461bcd60e51b815260206004820152603860248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f74206f7760448201527f6e6572206e6f7220617070726f76656420666f7220616c6c00000000000000006064820152608401610e87565b610ce18383613834565b610fc56138a2565b6001600160a01b0382166000818152600d6020908152604091829020805460ff191685151590811790915591514281529192917f8f617843889b94892bd44852d36ca6a7f49ecf4350a01e7b68e22d80f4ed95bc91015b60405180910390a35050565b600080838360405161103b9291906153a9565b6040805191829003909120600090815260126020522054949350505050565b61106433826138cd565b6110805760405162461bcd60e51b8152600401610e87906153b9565b610ce18383836139c0565b600061109683611ace565b82106110f85760405162461bcd60e51b815260206004820152602b60248201527f455243373231456e756d657261626c653a206f776e657220696e646578206f7560448201526a74206f6620626f756e647360a81b6064820152608401610e87565b506001600160a01b03919091166000908152600660209081526040808320938352929052205490565b600061112b613b6b565b611135823561362c565b610cbf6111418361540a565b613ba2565b919050565b6111536138a2565b6001600160a01b0382166000818152600f6020908152604091829020805460ff191685151590811790915591514281529192917f6cc19a794d6a439023150cd58748eed4353190c0bb060d2e6250e2df4a68b673910161101c565b60008115806111ca575060008381526013602052604090205482115b156111d757506003610cbf565b60008381526014602090815260408083208584529091529020600401546001600160a01b031661120957506002610cbf565b600083815260146020908152604080832085845290915290205461122f57506000610cbf565b506001610cbf565b61123f6138a2565b601880546001600160a01b038381166001600160a01b03198316811790935560405191169190829033907f676c0801b0f400762e958ee31cfbb10870e70786f6761f57c8647e766b0db3d9906112989042815260200190565b60405180910390a45050565b60008181526013602052604090206005018054606091906112c490615374565b80601f01602080910402602001604051908101604052809291908181526020018280546112f090615374565b801561133d5780601f106113125761010080835404028352916020019161133d565b820191906000526020600020905b81548152906001019060200180831161132057829003601f168201915b50505050509050919050565b6113516135f2565b600061135d82356119c0565b905061144761143d7f61332f0343256b019dca2e0ffc75766fde55292d645df285bb1a3b74a493b51084356113986040870160208801614b22565b6113a560408801886154a5565b6040516113b39291906153a9565b604080519182900382206001600160a01b0389166000908152600a6020908152929020805460018101909155611422969594939192909160c08c0135910195865260208601949094526001600160a01b039290921660408501526060840152608083015260a082015260c00190565b60405160208183030381529060405280519060200120613bfa565b8284606001613c44565b73__$1f7cbacb1f9f5d323b85b0487838426c8d$__636a7ecf1383356114736040860160208701614b22565b61148060408701876154a5565b87356000908152601360205260409081902090516001600160e01b031960e088901b1681526114b9959493929190600e90600401615514565b60006040518083038186803b1580156114d157600080fd5b505af4158015610e0a573d6000803e3d6000fd5b60006114ef613b6b565b60006114fb83356119c0565b905061163661162c7ffb2be94758c5f2515e99eae2d54860ea468ce314ffb38e328c234793acbcccf4853561153360208801886154a5565b6040516115419291906153a9565b604051809103902087604001602081019061155c9190614b22565b61156960608a018a6154a5565b6040516115779291906153a9565b60405190819003902061159060a08b0160808c01614b22565b61159d60a08c018c6154a5565b6040516115ab9291906153a9565b604080519182900382206001600160a01b03808d166000908152600a6020908152908490208054600181019091559085019a909a52918301979097526060820195909552928416608084015260a083019190915290911660c082015260e0810191909152610100810191909152610120808701359082015261014001611422565b828560c001613c44565b611726833561164860208601866154a5565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061168d925050506060870160408801614b22565b61169a60608801886154a5565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506116df9250505060a0890160808a01614b22565b6116ec60a08a018a6154a5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250613d2292505050565b9392505050565b6117356138a2565b6001600160a01b038216600081815260106020908152604091829020805460ff191685151590811790915591514281529192917f37872a053ef20cb52defb7c9ec20e1a87cb8dd5098ac9e76a144be263dfef572910161101c565b610ce183838360405180602001604052806000815250612937565b6117b36135f2565b6117bc81613dc1565b6117c581613df1565b50565b6000818152600260205260408120546001600160a01b03161515610cbf565b60006117f260085490565b82106118555760405162461bcd60e51b815260206004820152602c60248201527f455243373231456e756d657261626c653a20676c6f62616c20696e646578206f60448201526b7574206f6620626f756e647360a01b6064820152608401610e87565b6008828154811061186857611868615554565b90600052602060002001549050919050565b600081815260026020526040812054600160a01b90046001600160601b0316806119015760405162461bcd60e51b815260206004820152603260248201527f4552433732313a206d696e742074696d657374616d7020717565727920666f72604482015271103737b732bc34b9ba32b73a103a37b5b2b760711b6064820152608401610e87565b6001600160601b031692915050565b6018546001600160a01b031633141561196357600081600281111561193757611937614af2565b141561195657604051630357a1ab60e31b815260040160405180910390fd5b61195e6135f2565b61198e565b6017546001600160a01b0316331461198e57604051635010559960e11b815260040160405180910390fd5b6117c58161375d565b6000828152601460209081526040808320848452909152902080546001909101545b9250929050565b6000818152600260205260408120546001600160a01b031680610cbf5760405162461bcd60e51b815260206004820152602960248201527f4552433732313a206f776e657220717565727920666f72206e6f6e657869737460448201526832b73a103a37b5b2b760b91b6064820152608401610e87565b611a3f6135f2565b611a4884613e2c565b600084815260136020526040908190209051636a7ecf1360e01b815273__$1f7cbacb1f9f5d323b85b0487838426c8d$__91636a7ecf1391611a9891889188918891889190600e90600401615514565b60006040518083038186803b158015611ab057600080fd5b505af4158015611ac4573d6000803e3d6000fd5b5050505050505050565b60006001600160a01b038216611b395760405162461bcd60e51b815260206004820152602a60248201527f4552433732313a2062616c616e636520717565727920666f7220746865207a65604482015269726f206164647265737360b01b6064820152608401610e87565b506001600160a01b031660009081526003602052604090205490565b6000611b5f613b6b565b6000611b6b83356119c0565b9050611d14611d097f35b861ea91c543d61c3f094b5effa29dfcd22a439290ae248fdccbc7162016918535611ba360208801886154a5565b604051611bb19291906153a9565b604051809103902087604001358860600135898060800190611bd391906154a5565b604051611be19291906153a9565b604051908190039020611bfa60c08c0160a08d01614b22565b611c0760c08d018d6154a5565b604051611c159291906153a9565b604051908190039020611c2f6101008e0160e08f01614b22565b611c3d6101008f018f6154a5565b604051611c4b9291906153a9565b6040518091039020600a60008e6001600160a01b03166001600160a01b031681526020019081526020016000206000815480929190600101919050558f61012001606001356040516020016114229c9b9a999897969594939291909b8c5260208c019a909a5260408b019890985260608a0196909652608089019490945260a08801929092526001600160a01b0390811660c088015260e0870191909152166101008501526101208401526101408301526101608201526101800190565b828561012001613c44565b61172660405180610120016040528085600001358152602001858060200190611d3d91906154a5565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050509082525060408087013560208301526060808801359183019190915201611d9a60808701876154a5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250505090825250602001611de460c0870160a08801614b22565b6001600160a01b03168152602001611dff60c08701876154a5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250505090825250602001611e4a610100870160e08801614b22565b6001600160a01b03168152602001611e666101008701876154a5565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505050915250613e66565b6001600160a01b038316611ec8576040516307eb16dd60e21b815260040160405180910390fd5b6000611ed3836119c0565b6001600160a01b038181166000908152600a602090815260409182902080546001810190915582517f49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad9281019290925292881691810191909152606080820187905260808201929092529084013560a0820152909150611f6090611f599060c001611422565b8284613c44565b611f6a8484613834565b50505050565b6000611f7a6135f2565b60405163334ce6f160e21b815273__$1e68a60ae0444699fe08192a29ecc09930$__9063cd339bc490611fe190339089908990899089907f00000000000000000000000000000000000000000000000000000000000000009060149060139060040161556a565b602060405180830381865af4158015611ffe573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061202291906155b8565b90505b949350505050565b60008581526014602090815260408083208784529091529020600501546001600160a01b03163381146120735760405163c6d1651b60e01b815260040160405180910390fd5b604080516001600160a01b03858116825284166020820152428183015290518591879189917f68edb7ec2c37d21b3b72233960b487f2966f4ac82b7430d39f24d1f8d6f99106919081900360600190a4505050505050565b6001600160a01b0383166120f2576040516307eb16dd60e21b815260040160405180910390fd5b6001600160a01b038481166000818152600a602090815260409182902080546001810190915582517f47ab88482c90e4bb94b82a947ae78fa91fb25de1469ab491f4c15b9a0a2677ee9281019290925291810192909252918516606080830191909152841515608083015260a08201929092529082013560c08201526121859061217e9060e001611422565b8583613c44565b611f6a848484613ec5565b606061219a6135f2565b60006121a960408401846155d1565b905090506000816001600160401b038111156121c7576121c7614f9d565b6040519080825280602002602001820160405280156121f0578160200160208202803683370190505b50905060005b828110156122685761220b60408601866155d1565b8281811061221b5761221b615554565b905060200281019061222d91906154a5565b60405161223b9291906153a9565b604051809103902082828151811061225557612255615554565b60209081029190910101526001016121f6565b5061236a6123547ffb6b7f1cd1b38daf3822aff0abbe78124db5d62a4748bcff007c15ccd6d30bc561229d60208801886155d1565b6040516020016122ae92919061561a565b60405160208183030381529060405280519060200120846040516020016122d59190615646565b60405160208183030381529060405280519060200120600a60008a60000160208101906123029190614b22565b6001600160a01b0316815260208082019290925260409081016000208054600181019091558151928301959095528101929092526060820152608081019190915260c08781013560a083015201611422565b6123616020870187614b22565b86606001613c44565b73__$1e68a60ae0444699fe08192a29ecc09930$__636fb4b4636123916020870187614b22565b61239e60208801886155d1565b6123ab60408a018a6155d1565b601360126040518863ffffffff1660e01b81526004016123d197969594939291906156c1565b600060405180830381865af41580156123ee573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052612025919081019061577b565b606060018054610cf590615374565b600061242f613b6b565b612439823561362c565b610cbf823561244b60208501856154a5565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250612490925050506060860160408701614b22565b61249d60608701876154a5565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506124e29250505060a0880160808901614b22565b6116ec60a08901896154a5565b6124f76135f2565b600061250382356119c0565b90506125b06125a67f5b9860bd835e648945b22d053515bc1f53b7d9fab4b23b1b49db15722e945d14843561253b60208701876154a5565b6040516125499291906153a9565b604080519182900382206001600160a01b0388166000908152600a60209081529083902080546001810190915590840195909552908201929092526060810191909152608081019190915260a0858101359082015260c001611422565b8284604001613c44565b6125c782356125c260208501856154a5565b61368b565b5050565b6001600160a01b0382163314156126245760405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c6572000000000000006044820152606401610e87565b6125c7338383613ec5565b6126376138a2565b6001600160a01b0382166000818152600e6020908152604091829020805460ff191685151590811790915591514281529192917f52c5b7889df9f12f84ec3da051e854e5876678370d8357959c23ef59dd6486df910161101c565b61269a6138a2565b6117c5816137db565b60006126ad6135f2565b61278361276d7f7f9b4ea1fc678b4fda1611ac5cbd28f339e235d89b1540635e9b2e0223a3c101602085013560408601356126eb60608801886154a5565b6040516126f99291906153a9565b604051908190039020600a600061271360208b018b614b22565b6001600160a01b0316815260208082019290925260409081016000208054600181019091558151928301969096528101939093526060830191909152608082015260a081019190915260e08581013560c083015201611422565b61277a6020850185614b22565b84608001613c44565b73__$1e68a60ae0444699fe08192a29ecc09930$__63cd339bc46127aa6020850185614b22565b602085013560408601356127c160608801886154a5565b7f0000000000000000000000000000000000000000000000000000000000000000601460136040518963ffffffff1660e01b815260040161280998979695949392919061556a565b602060405180830381865af4158015612826573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cbf91906155b8565b606060008061285b85856014613f32565b506000828152601460209081526040808320848452909152902060020180549294509092509061288a90615374565b80601f01602080910402602001604051908101604052809291908181526020018280546128b690615374565b80156129035780601f106128d857610100808354040283529160200191612903565b820191906000526020600020905b8154815290600101906020018083116128e657829003601f168201915b50505050509250505092915050565b600061291c613b6b565b612926823561362c565b610cbf61293283615820565b613e66565b61294133836138cd565b61295d5760405162461bcd60e51b8152600401610e87906153b9565b611f6a84848484613ff4565b6129716135f2565b600061297d82356119c0565b90506129b56125a67fd8d76e8b2b26e1ebe72def13fb559a68561ef064055b0de01f955bc26e25d42f843561253b60208701876154a5565b6125c782356129c760208501856154a5565b614027565b6000848152601360205260409020600201546001600160a01b0316338114612a075760405163646785d560e11b815260040160405180910390fd5b604080516001600160a01b0385811682528416602082015242818301529051859187917f4996ad2257e7db44908136c43128cc10ca988096f67dc6bb0bcee11d151368fb9181900360600190a35050505050565b612a636135f2565b6000612a6f82356119c0565b9050612b006125a67f77ba3e9f5fa75343bbad1241fb539a0064de97694b47d463d1eb5c54aba11f0f8435612aaa6040870160208801614b22565b6001600160a01b038681166000908152600a602090815260409182902080546001810190915582519182019690965290810193909352166060820152608081019190915260a0858101359082015260c001611422565b6125c78235612b156040850160208601614b22565b614078565b612b226135f2565b612b2b82613e2c565b6125c78282614078565b60408051808201909152600080825260208201526000828152600260205260409020546001600160a01b0316612bc45760405162461bcd60e51b815260206004820152602e60248201527f4552433732313a20746f6b656e206461746120717565727920666f72206e6f6e60448201526d32bc34b9ba32b73a103a37b5b2b760911b6064820152608401610e87565b506000908152600260209081526040918290208251808401909352546001600160a01b0381168352600160a01b90046001600160601b03169082015290565b612c0b6135f2565b612c148361362c565b610ce1838383614027565b612c736040518060c0016040528060008152602001600081526020016060815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b031681525090565b6014600084815260200190815260200160002060008381526020019081526020016000206040518060c00160405290816000820154815260200160018201548152602001600282018054612cc690615374565b80601f0160208091040260200160405190810160405280929190818152602001828054612cf290615374565b8015612d3f5780601f10612d1457610100808354040283529160200191612d3f565b820191906000526020600020905b815481529060010190602001808311612d2257829003601f168201915b505050918352505060038201546001600160a01b0390811660208301526004830154811660408301526005909201549091166060909101529392505050565b6000818152601360205260409020600201546060906001600160a01b031673__$f906f20d797116ee89ed79945048c6ad36$__633edea73c848315612e2457836001600160a01b03166318160ddd6040518163ffffffff1660e01b8152600401602060405180830381865afa158015612dfb573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612e1f91906155b8565b612e27565b60005b612e30876119c0565b6000888152601360205260409081902090516001600160e01b031960e087901b168152612e6b94939291600381019160049182019101615993565b600060405180830381865af4158015612e88573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261172691908101906159d2565b612eb86135f2565b612f79612f637e198a25e19615d82f53977db0fc1ee7f3701b0fdf1299a971cf997545879c04612eeb6020850185614b22565b60208501803590600a90600090612f029089614b22565b6001600160a01b03168152602080820192909252604090810160002080546001810190915590516114229594939260a08a013591019485526001600160a01b0393909316602085015260408401919091526060830152608082015260a00190565b612f706020840184614b22565b83604001613c44565b6117c5612f896020830183614b22565b82602001356140d5565b612f9b6135f2565b612fa5828261416d565b6125c782613df1565b6000612fb8613b6b565b6000612fc483356119c0565b90506130c961162c7f4178441487eb967c15821c9ffee94853812f263a69ccb6352bdaac59f77e486f85356020870135604088013561300660608a018a6154a5565b6040516130149291906153a9565b60405190819003902061302d60a08b0160808c01614b22565b61303a60a08c018c6154a5565b6040516130489291906153a9565b604080519182900382206001600160a01b03808d166000908152600a6020908152908490208054600181019091559085019a909a52918301979097526060820195909552608081019390935260a083019190915290911660c082015260e0810191909152610100810191909152610120808701359082015261014001611422565b6117266040518060c0016040528085600001358152602001856020013581526020018560400135815260200185806060019061310591906154a5565b8080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050509082525060200161314f60a0870160808801614b22565b6001600160a01b0316815260200161316a60a08701876154a5565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152505050915250613ba2565b60008181526013602052604090206003018054606091906112c490615374565b60006131cf6141fb565b905090565b61321f6040518060c001604052806000815260200160006001600160a01b0316815260200160006001600160a01b031681526020016060815260200160608152602001606081525090565b600082815260136020908152604091829020825160c0810184528154815260018201546001600160a01b039081169382019390935260028201549092169282019290925260038201805491929160608401919061327b90615374565b80601f01602080910402602001604051908101604052809291908181526020018280546132a790615374565b80156132f45780601f106132c9576101008083540402835291602001916132f4565b820191906000526020600020905b8154815290600101906020018083116132d757829003601f168201915b5050505050815260200160048201805461330d90615374565b80601f016020809104026020016040519081016040528092919081815260200182805461333990615374565b80156133865780601f1061335b57610100808354040283529160200191613386565b820191906000526020600020905b81548152906001019060200180831161336957829003601f168201915b5050505050815260200160058201805461339f90615374565b80601f01602080910402602001604051908101604052809291908181526020018280546133cb90615374565b80156134185780601f106133ed57610100808354040283529160200191613418565b820191906000526020600020905b8154815290600101906020018083116133fb57829003601f168201915b5050505050815250509050919050565b6134306135f2565b6117c533826140d5565b60606134446135f2565b604051636fb4b46360e01b815273__$1e68a60ae0444699fe08192a29ecc09930$__90636fb4b4639061348990339089908990899089906013906012906004016156c1565b600060405180830381865af41580156134a6573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052612022919081019061577b565b60006134d86135f2565b336000908152600d602052604090205460ff166135085760405163561a858760e01b815260040160405180910390fd5b60168054600101908190556135296135236020850185614b22565b82614290565b604051633f67a66f60e11b815273__$1f7cbacb1f9f5d323b85b0487838426c8d$__90637ecf4cde9061356b9086908590601290601390600e90600401615a3f565b60006040518083038186803b15801561358357600080fd5b505af4158015613597573d6000803e3d6000fd5b509295945050505050565b60006001600160e01b031982166380ac58cd60e01b14806135d357506001600160e01b03198216635b5e139f60e01b145b80610cbf57506301ffc9a760e01b6001600160e01b0319831614610cbf565b6002600c5460ff16600281111561360b5761360b614af2565b141561362a576040516313d0ff5960e31b815260040160405180910390fd5b565b613635816119c0565b6001600160a01b0316336001600160a01b0316148061366a57506000818152601160205260409020546001600160a01b031633145b156136725750565b60405163f33ac98f60e01b815260040160405180910390fd5b6117708111156136ae5760405163781548c760e01b815260040160405180910390fd5b60008381526013602052604090206136ca906004018383614855565b50827fd5a5879cad33c830cc1432c1850107029a09c80c60e9bce1ecd08d24880bd46c8383426040516136ff93929190615b36565b60405180910390a2505050565b613718848484846143e1565b7f414cd0b34676984f09a5f76ce9718d4062e50283abe0e7e274a9a5b4e0c99c30848484844260405161374f959493929190615b5a565b60405180910390a150505050565b600c805460ff811691839160ff1916600183600281111561378057613780614af2565b021790555081600281111561379757613797614af2565b8160028111156137a9576137a9614af2565b60405142815233907fa2f9a1499fc1f9b7796d21fe5761290ccb7e0ef6ccf35fa58b668f304a62a1ca90602001611298565b601780546001600160a01b038381166001600160a01b03198316811790935560405191169190829033907fe552a55455b740845a5c07ed445d1724142fc997b389835495a29b30cddc1ccd906112989042815260200190565b600081815260046020526040902080546001600160a01b0319166001600160a01b0384169081179091558190613869826119c0565b6001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b6017546001600160a01b0316331461362a57604051632d5be4cb60e21b815260040160405180910390fd5b6000818152600260205260408120546001600160a01b03166139465760405162461bcd60e51b815260206004820152602c60248201527f4552433732313a206f70657261746f7220717565727920666f72206e6f6e657860448201526b34b9ba32b73a103a37b5b2b760a11b6064820152608401610e87565b6000613951836119c0565b9050806001600160a01b0316846001600160a01b0316148061398c5750836001600160a01b031661398184610e12565b6001600160a01b0316145b8061202557506001600160a01b0380821660009081526005602090815260408083209388168352929052205460ff16612025565b826001600160a01b03166139d3826119c0565b6001600160a01b031614613a3b5760405162461bcd60e51b815260206004820152602960248201527f4552433732313a207472616e73666572206f6620746f6b656e2074686174206960448201526839903737ba1037bbb760b91b6064820152608401610e87565b6001600160a01b038216613a9d5760405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f206164646044820152637265737360e01b6064820152608401610e87565b613aa8838383614401565b613ab3600082613834565b6001600160a01b0383166000908152600360205260408120805460019290613adc908490615baa565b90915550506001600160a01b0382166000908152600360205260408120805460019290613b0a908490615bc1565b909155505060008181526002602052604080822080546001600160a01b0319166001600160a01b0386811691821790925591518493918716917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91a4505050565b6000600c5460ff166002811115613b8457613b84614af2565b1461362a57604051630f392a3b60e31b815260040160405180910390fd5b8051600090815260136020526040808220805460010190819055905163764e3e9360e01b815273__$1f7cbacb1f9f5d323b85b0487838426c8d$__9063764e3e939061356b9086908590601490601090600401615bd9565b600080613c056141fb565b60405161190160f01b602082015260228101919091526042810184905260620160408051601f1981840301815291905280516020909101209392505050565b4281606001351015613c6957604051630819bdcd60e01b815260040160405180910390fd5b6000600184613c7b6020850185615c65565b604080516000815260208181018084529490945260ff9092168282015291850135606082015290840135608082015260a0016020604051602081039080840390855afa158015613ccf573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b0381161580613d045750826001600160a01b0316816001600160a01b031614155b15611f6a576040516337e8456b60e01b815260040160405180910390fd5b60008681526013602052604080822080546001019081905590516309dbf05160e41b815273__$1f7cbacb1f9f5d323b85b0487838426c8d$__90639dbf051090613d85908b908b908b908b908b908b908a90601490600f90601090600401615c88565b60006040518083038186803b158015613d9d57600080fd5b505af4158015613db1573d6000803e3d6000fd5b50929a9950505050505050505050565b613dcb33826138cd565b613de857604051636d8a29e760e11b815260040160405180910390fd5b6117c581614476565b6000818152601360205260408082209051613e0f9160030190615d0f565b604080519182900390912060009081526012602052908120555050565b613e35816119c0565b6001600160a01b0316336001600160a01b0316146117c55760405163f194fae560e01b815260040160405180910390fd5b80516000908152601360208190526040808320805460010190819055905163731d549f60e11b8152909173__$1f7cbacb1f9f5d323b85b0487838426c8d$__9163e63aa93e9161356b9187918691601490600f90601090600401615d7d565b6001600160a01b03838116600081815260056020908152604080832094871680845294825291829020805460ff191686151590811790915591519182527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31910160405180910390a3505050565b600083815260208281526040808320858452909152812060040154819081906001600160a01b03168015613f6d578693508592509050613feb565b60008781526020868152604080832089845290915290205480613fa35760405163a43d2a7160e01b815260040160405180910390fd5b6000888152602087815260408083208a84528252808320600101548484528983528184208185529092529091206004015491955093506001600160a01b03169150613feb9050565b93509350939050565b613fff8484846139c0565b61400b84848484614512565b611f6a5760405162461bcd60e51b8152600401610e8790615e7f565b6000838152601360205260409020614043906005018383614855565b50827fe82886e1af6fcab5caef13815b22f51384e970c367a785f265d13860a7d6966d8383426040516136ff93929190615b36565b60008281526011602090815260409182902080546001600160a01b0319166001600160a01b038516908117909155915142815284917f22baaec4952f35f59e45bd2ddb287e1ccc6d319375770c09428eb8f8d604e065910161101c565b6000811180156140ff57506140e9816119c0565b6001600160a01b0316826001600160a01b031614155b1561411d5760405163f194fae560e01b815260040160405180910390fd5b6001600160a01b03821660008181526015602052604090819020839055518291907f0afd7c479e8bc7dcdb856b3cc27d2332dfe1f018fde574ea124919ddcae8a9339061101c9042815260200190565b6000614178836119c0565b6001600160a01b0381166000908152600a602090815260409182902080546001810190915582517f108ccda6d7331b00561a3eea66a2ae331622356585681c62731e4a01aae2261a928101929092529181018690526060808201929092529084013560808201529091506141f290611f599060a001611422565b610ce183614476565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f614226610ce6565b80516020918201206040805192830193909352918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b6001600160a01b0382166142e65760405162461bcd60e51b815260206004820181905260248201527f4552433732313a206d696e7420746f20746865207a65726f20616464726573736044820152606401610e87565b6000818152600260205260409020546001600160a01b03161561434b5760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e746564000000006044820152606401610e87565b61435760008383614401565b6001600160a01b0382166000908152600360205260408120805460019290614380908490615bc1565b90915550506000818152600260205260408082206001600160a01b038516600160a01b426001600160601b031602811790915590518392907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908290a45050565b6143ed60008585614855565b506143fa60018383614855565b5050505050565b6144096135f2565b6000818152601160205260409020546001600160a01b03161561443157614431816000614078565b6001600160a01b03831660009081526015602052604090205481141561446b576001600160a01b0383166000908152601560205260408120555b610ce183838361460d565b6000614481826119c0565b905061448f81600084614401565b61449a600083613834565b6001600160a01b03811660009081526003602052604081208054600192906144c3908490615baa565b9091555050600082815260026020526040808220829055518391906001600160a01b038416907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908390a45050565b60006001600160a01b0384163b1561460557604051630a85bd0160e11b81526001600160a01b0385169063150b7a0290614556903390899088908890600401615ed1565b6020604051808303816000875af1925050508015614591575060408051601f3d908101601f1916820190925261458e91810190615f0e565b60015b6145eb573d8080156145bf576040519150601f19603f3d011682016040523d82523d6000602084013e6145c4565b606091505b5080516145e35760405162461bcd60e51b8152600401610e8790615e7f565b805181602001fd5b6001600160e01b031916630a85bd0160e11b149050612025565b506001612025565b6001600160a01b0383166146685761466381600880546000838152600960205260408120829055600182018355919091527ff3f7a9fe364faab93b216da50a3214154f22a0a2b415b23a84c8169e8b636ee30155565b61468b565b816001600160a01b0316836001600160a01b03161461468b5761468b83826146c5565b6001600160a01b0382166146a257610ce181614762565b826001600160a01b0316826001600160a01b031614610ce157610ce18282614811565b600060016146d284611ace565b6146dc9190615baa565b60008381526007602052604090205490915080821461472f576001600160a01b03841660009081526006602090815260408083208584528252808320548484528184208190558352600790915290208190555b5060009182526007602090815260408084208490556001600160a01b039094168352600681528383209183525290812055565b60085460009061477490600190615baa565b6000838152600960205260408120546008805493945090928490811061479c5761479c615554565b9060005260206000200154905080600883815481106147bd576147bd615554565b60009182526020808320909101929092558281526009909152604080822084905585825281205560088054806147f5576147f5615f2b565b6001900381819060005260206000200160009055905550505050565b600061481c83611ace565b6001600160a01b039093166000908152600660209081526040808320868452825280832085905593825260079052919091209190915550565b82805461486190615374565b90600052602060002090601f01602090048101928261488357600085556148c9565b82601f1061489c5782800160ff198235161785556148c9565b828001600101855582156148c9579182015b828111156148c95782358255916020019190600101906148ae565b506148d59291506148d9565b5090565b5b808211156148d557600081556001016148da565b6001600160e01b0319811681146117c557600080fd5b60006020828403121561491657600080fd5b8135611726816148ee565b60008083601f84011261493357600080fd5b5081356001600160401b0381111561494a57600080fd5b6020830191508360208285010111156119b957600080fd5b60008060006040848603121561497757600080fd5b8335925060208401356001600160401b0381111561499457600080fd5b6149a086828701614921565b9497909650939450505050565b60005b838110156149c85781810151838201526020016149b0565b83811115611f6a5750506000910152565b600081518084526149f18160208601602086016149ad565b601f01601f19169290920160200192915050565b60208152600061172660208301846149d9565b80356001600160a01b038116811461114657600080fd5b600080600080600060608688031215614a4757600080fd5b85356001600160401b0380821115614a5e57600080fd5b614a6a89838a01614921565b90975095506020880135915080821115614a8357600080fd5b50614a9088828901614921565b9094509250614aa3905060408701614a18565b90509295509295909350565b600060208284031215614ac157600080fd5b5035919050565b60008060408385031215614adb57600080fd5b614ae483614a18565b946020939093013593505050565b634e487b7160e01b600052602160045260246000fd5b6020810160038310614b1c57614b1c614af2565b91905290565b600060208284031215614b3457600080fd5b61172682614a18565b8035801515811461114657600080fd5b60008060408385031215614b6057600080fd5b614b6983614a18565b9150614b7760208401614b3d565b90509250929050565b60008060208385031215614b9357600080fd5b82356001600160401b03811115614ba957600080fd5b614bb585828601614921565b90969095509350505050565b600080600060608486031215614bd657600080fd5b614bdf84614a18565b9250614bed60208501614a18565b9150604084013590509250925092565b600060c08284031215614c0f57600080fd5b50919050565b600060208284031215614c2757600080fd5b81356001600160401b03811115614c3d57600080fd5b61202584828501614bfd565b60008060408385031215614c5c57600080fd5b50508035926020909101359150565b6020810160048310614b1c57614b1c614af2565b600060e08284031215614c0f57600080fd5b600060208284031215614ca357600080fd5b81356001600160401b03811115614cb957600080fd5b61202584828501614c7f565b60006101408284031215614c0f57600080fd5b600060208284031215614cea57600080fd5b81356001600160401b03811115614d0057600080fd5b61202584828501614cc5565b600060208284031215614d1e57600080fd5b81356003811061172657600080fd5b60008060008060608587031215614d4357600080fd5b84359350614d5360208601614a18565b925060408501356001600160401b03811115614d6e57600080fd5b614d7a87828801614921565b95989497509550505050565b600060208284031215614d9857600080fd5b81356001600160401b03811115614dae57600080fd5b82016101a0818503121561172657600080fd5b600060808284031215614c0f57600080fd5b600080600060c08486031215614de857600080fd5b614df184614a18565b925060208401359150614e078560408601614dc1565b90509250925092565b60008060008060608587031215614e2657600080fd5b843593506020850135925060408501356001600160401b03811115614d6e57600080fd5b600080600080600060a08688031215614e6257600080fd5b853594506020860135935060408601359250614e8060608701614a18565b9150614aa360808701614a18565b60008060008060e08587031215614ea457600080fd5b614ead85614a18565b9350614ebb60208601614a18565b9250614ec960408601614b3d565b9150614ed88660608701614dc1565b905092959194509250565b6020808252825182820181905260009190848201906040850190845b81811015614f1b57835183529284019291840191600101614eff565b50909695505050505050565b600060208284031215614f3957600080fd5b81356001600160401b03811115614f4f57600080fd5b8201610100818503121561172657600080fd5b600060208284031215614f7457600080fd5b81356001600160401b03811115614f8a57600080fd5b8201610120818503121561172657600080fd5b634e487b7160e01b600052604160045260246000fd5b60405160c081016001600160401b0381118282101715614fd557614fd5614f9d565b60405290565b60405161012081016001600160401b0381118282101715614fd557614fd5614f9d565b604051601f8201601f191681016001600160401b038111828210171561502657615026614f9d565b604052919050565b60006001600160401b0382111561504757615047614f9d565b50601f01601f191660200190565b600082601f83011261506657600080fd5b81356150796150748261502e565b614ffe565b81815284602083860101111561508e57600080fd5b816020850160208301376000918101602001919091529392505050565b600080600080608085870312156150c157600080fd5b6150ca85614a18565b93506150d860208601614a18565b92506040850135915060608501356001600160401b038111156150fa57600080fd5b61510687828801615055565b91505092959194509250565b6000806000806080858703121561512857600080fd5b843593506020850135925061513f60408601614a18565b9150614ed860608601614a18565b600060c0828403121561515f57600080fd5b6117268383614bfd565b6000806040838503121561517c57600080fd5b82359150614b7760208401614a18565b6020815281516020820152602082015160408201526000604083015160c060608401526151bc60e08401826149d9565b9050606084015160018060a01b0380821660808601528060808701511660a08601528060a08701511660c086015250508091505092915050565b60008060a0838503121561520957600080fd5b82359150614b778460208501614dc1565b6000806040838503121561522d57600080fd5b61523683614a18565b9150614b7760208401614a18565b60208152815160208201526000602083015160018060a01b0380821660408501528060408601511660608501525050606083015160c0608084015261528c60e08401826149d9565b90506080840151601f19808584030160a08601526152aa83836149d9565b925060a08601519150808584030160c0860152506152c882826149d9565b95945050505050565b60008083601f8401126152e357600080fd5b5081356001600160401b038111156152fa57600080fd5b6020830191508360208260051b85010111156119b957600080fd5b6000806000806040858703121561532b57600080fd5b84356001600160401b038082111561534257600080fd5b61534e888389016152d1565b9096509450602087013591508082111561536757600080fd5b50614d7a878288016152d1565b600181811c9082168061538857607f821691505b60208210811415614c0f57634e487b7160e01b600052602260045260246000fd5b8183823760009101908152919050565b60208082526031908201527f4552433732313a207472616e736665722063616c6c6572206973206e6f74206f6040820152701ddb995c881b9bdc88185c1c1c9bdd9959607a1b606082015260800190565b600060c0823603121561541c57600080fd5b615424614fb3565b82358152602083013560208201526040830135604082015260608301356001600160401b038082111561545657600080fd5b61546236838701615055565b606084015261547360808601614a18565b608084015260a085013591508082111561548c57600080fd5b5061549936828601615055565b60a08301525092915050565b6000808335601e198436030181126154bc57600080fd5b8301803591506001600160401b038211156154d657600080fd5b6020019150368190038213156119b957600080fd5b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b8681526001600160a01b038616602082015260a06040820181905260009061553f90830186886154eb565b60608301949094525060800152949350505050565b634e487b7160e01b600052603260045260246000fd5b600060018060a01b03808b16835289602084015288604084015260e0606084015261559960e08401888a6154eb565b951660808301525060a081019290925260c09091015295945050505050565b6000602082840312156155ca57600080fd5b5051919050565b6000808335601e198436030181126155e857600080fd5b8301803591506001600160401b0382111561560257600080fd5b6020019150600581901b36038213156119b957600080fd5b60006001600160fb1b0383111561563057600080fd5b8260051b80858437600092019182525092915050565b815160009082906020808601845b8381101561567057815185529382019390820190600101615654565b50929695505050505050565b6000808335601e1984360301811261569357600080fd5b83016020810192503590506001600160401b038111156156b257600080fd5b8036038313156119b957600080fd5b6001600160a01b038816815260a060208083018290529082018790526000906001600160fb1b038811156156f457600080fd5b8760051b808a60c0860137830183810360c090810160408601528101879052600587901b810160e09081019082018960005b8a8110156157605784840360df19018352615741828d61567c565b61574c8682846154eb565b955050509185019190850190600101615726565b50505060608501969096525050506080015295945050505050565b6000602080838503121561578e57600080fd5b82516001600160401b03808211156157a557600080fd5b818501915085601f8301126157b957600080fd5b8151818111156157cb576157cb614f9d565b8060051b91506157dc848301614ffe565b81815291830184019184810190888411156157f657600080fd5b938501935b83851015615814578451825293850193908501906157fb565b98975050505050505050565b6000610120823603121561583357600080fd5b61583b614fdb565b8235815260208301356001600160401b038082111561585957600080fd5b61586536838701615055565b60208401526040850135604084015260608501356060840152608085013591508082111561589257600080fd5b61589e36838701615055565b60808401526158af60a08601614a18565b60a084015260c08501359150808211156158c857600080fd5b6158d436838701615055565b60c08401526158e560e08601614a18565b60e08401526101009150818501358181111561590057600080fd5b61590c36828801615055565b8385015250505080915050919050565b6000815461592981615374565b808552602060018381168015615946576001811461595a57615988565b60ff19851688840152604088019550615988565b866000528260002060005b858110156159805781548a8201860152908301908401615965565b890184019650505b505050505092915050565b85815284602082015260018060a01b038416604082015260a0606082015260006159c060a083018561591c565b8281036080840152615814818561591c565b6000602082840312156159e457600080fd5b81516001600160401b038111156159fa57600080fd5b8201601f81018413615a0b57600080fd5b8051615a196150748261502e565b818152856020838501011115615a2e57600080fd5b6152c88260208301602086016149ad565b60a0815260006001600160a01b0380615a5789614a18565b1660a0840152615a6a602089018961567c565b60c080860152615a7f610160860182846154eb565b915050615a8f60408a018a61567c565b609f19808785030160e0880152615aa78483856154eb565b935084615ab660608e01614a18565b16610100880152615aca60808d018d61567c565b955092508087850301610120880152615ae48486856154eb565b9450615af360a08d018d61567c565b9450925080878603016101408801525050615b0f8383836154eb565b93505050508560208301528460408301528360608301528260808301529695505050505050565b604081526000615b4a6040830185876154eb565b9050826020830152949350505050565b606081526000615b6e6060830187896154eb565b8281036020840152615b818186886154eb565b9150508260408301529695505050505050565b634e487b7160e01b600052601160045260246000fd5b600082821015615bbc57615bbc615b94565b500390565b60008219821115615bd457615bd4615b94565b500190565b6080815284516080820152602085015160a0820152604085015160c08201526000606086015160c060e0840152615c146101408401826149d9565b60808801516001600160a01b031661010085015260a0880151848203607f1901610120860152909150615c4782826149d9565b60208501979097525050506040810192909252606090910152919050565b600060208284031215615c7757600080fd5b813560ff8116811461172657600080fd5b60006101408c8352806020840152615ca28184018d6149d9565b6001600160a01b038c811660408601528482036060860152909150615cc7828c6149d9565b908a16608085015283810360a08501529050615ce381896149d9565b9150508560c08301528460e083015283610100830152826101208301529b9a5050505050505050505050565b6000808354615d1d81615374565b60018281168015615d355760018114615d4657615670565b60ff19841687528287019450615670565b8760005260208060002060005b85811015615d6c5781548a820152908401908201615d53565b505050959091019695505050505050565b60c08152865160c0820152600060208801516101208060e0850152615da66101e08501836149d9565b915060408a0151610100818187015260608c01518387015260808c0151925060bf1991508186850301610140870152615ddf84846149d9565b935060a08c01519250615dfe6101608701846001600160a01b03169052565b60c08c015192508186850301610180870152615e1a84846149d9565b935060e08c01519250615e396101a08701846001600160a01b03169052565b8b01518584039091016101c08601529050615e5482826149d9565b602085019990995250505060408101949094526060840192909252608083015260a090910152919050565b60208082526032908201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560408201527131b2b4bb32b91034b6b83632b6b2b73a32b960711b606082015260800190565b6001600160a01b0385811682528416602082015260408101839052608060608201819052600090615f04908301846149d9565b9695505050505050565b600060208284031215615f2057600080fd5b8151611726816148ee565b634e487b7160e01b600052603160045260246000fdfea2646970667358221220b22320e1e51caa663be0e995a3451f4dfdfa558ea5b06e71a5454229c1bc455b64736f6c634300080a0033", + "linkReferences": { + "contracts/libraries/InteractionLogic.sol": { + "InteractionLogic": [ + { + "length": 20, + "start": 8385 + }, + { + "length": 20, + "start": 9380 + }, + { + "length": 20, + "start": 10429 + }, + { + "length": 20, + "start": 13707 + } + ] + }, + "contracts/libraries/ProfileTokenURILogic.sol": { + "ProfileTokenURILogic": [ + { + "length": 20, + "start": 11990 + } + ] + }, + "contracts/libraries/PublishingLogic.sol": { + "PublishingLogic": [ + { + "length": 20, + "start": 5505 + }, + { + "length": 20, + "start": 7070 + }, + { + "length": 20, + "start": 13936 + }, + { + "length": 20, + "start": 15618 + }, + { + "length": 20, + "start": 16000 + }, + { + "length": 20, + "start": 16330 + } + ] + } + }, + "deployedLinkReferences": { + "contracts/libraries/InteractionLogic.sol": { + "InteractionLogic": [ + { + "length": 20, + "start": 8073 + }, + { + "length": 20, + "start": 9068 + }, + { + "length": 20, + "start": 10117 + }, + { + "length": 20, + "start": 13395 + } + ] + }, + "contracts/libraries/ProfileTokenURILogic.sol": { + "ProfileTokenURILogic": [ + { + "length": 20, + "start": 11678 + } + ] + }, + "contracts/libraries/PublishingLogic.sol": { + "PublishingLogic": [ + { + "length": 20, + "start": 5193 + }, + { + "length": 20, + "start": 6758 + }, + { + "length": 20, + "start": 13624 + }, + { + "length": 20, + "start": 15306 + }, + { + "length": 20, + "start": 15688 + }, + { + "length": 20, + "start": 16018 + } + ] + } + } +} diff --git a/tasks/helpers/constants.ts b/tasks/helpers/constants.ts index 77a383d..5c36b78 100644 --- a/tasks/helpers/constants.ts +++ b/tasks/helpers/constants.ts @@ -2,8 +2,11 @@ export const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffff export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; export const LZ_SOURCE_CHAINS = ['mumbai', 'polygon']; -export const LZ_DESTINATION_CHAINS = ['fantom_testnet', 'fantom']; +export const LZ_DESTINATION_CHAINS = ['fantom_testnet', 'fantom', 'goerli', 'optimismTestnet']; +// chainId: value not related to EVM IDs +// endpoint: LZEndpoint contract deployed on the network +// remote: the remote chain to be configured to send/receive messages from export const LZ_CONFIG = { fantom_testnet: { chainId: '10012', @@ -11,7 +14,7 @@ export const LZ_CONFIG = { remote: 'mumbai' }, mumbai: { - chainId: '10009', + chainId: '10109', endpoint: '0xf69186dfBa60DdB133E91E9A4B5673624293d8F8', remotes: ['bsc_testnet'] }, @@ -21,3 +24,37 @@ export const LZ_CONFIG = { remote: 'mumbai' } }; + +export const LZ_CONFIG_GATED_MODULES = { + mumbai: { + chainId: '10109', + endpoint: '0xf69186dfBa60DdB133E91E9A4B5673624293d8F8', + remotes: ['goerli', 'optimismTestnet'] + }, + goerli: { + chainId: '10121', + endpoint: '0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23', + remote: 'mumbai' + }, + optimismTestnet: { + chainId: '10132', + endpoint: '0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1', + remote: 'mumbai' + }, + // would need to add their hardhat plugin for deployment + // zkSync_testnet: { + // chainId: '10147', + // endpoint: '0xeaa8d1D0E736C59F7F0211C272d25f7AEC9FCB51', + // remote: 'mumbai' + // } +}; + +// @TODO: all the constants below should change per setup + +// https://hq.decent.xyz/5/Editions/0xBbD9a6186C084F7148FA9787E94828faF769c9A3 +export const TOKEN_CONTRACT = '0xBbD9a6186C084F7148FA9787E94828faF769c9A3'; // the ERC721 for token gate +export const TOKEN_THRESHOLD = '1'; // one token required to follow +export const TOKEN_CHAIN_ID = LZ_CONFIG_GATED_MODULES.goerli.chainId; // where our `TOKEN_CONTRACT` lives (goerli) + +export const SANDBOX_USER_PROFILE_ID = '322'; // thereisnosecondbest2.test +export const SANDBOX_USER_PROFILE_ID_USER = '329'; // lenstestwalletuser.test diff --git a/tasks/helpers/lens.ts b/tasks/helpers/lens.ts index ee12563..5ce9f09 100644 --- a/tasks/helpers/lens.ts +++ b/tasks/helpers/lens.ts @@ -1,9 +1,22 @@ -import { Contract } from "ethers"; +import { + Contract, + Signer, + BigNumber, + utils, + Bytes, +} from "ethers"; +import LensHub from './abi/LensHub.json'; +import { HARDHAT_CHAINID } from './../../test/lens/helpers/constants'; + +const LENS_DOMAIN_NAME = 'Lens Protocol Profiles'; +const LENS_DOMAIN_VERSION = '1'; const lensAddresses = { 'mumbai': { 'interaction logic lib': '0xefd400326635e016CbfCc309725D5B62FD9d3468', - 'lensHub proxy': '0x60Ae865ee4C725cd04353b5AAb364553f56ceF82' + 'lensHub proxy': '0x60Ae865ee4C725cd04353b5AAb364553f56ceF82', + 'lensHub sandbox': '0x7582177F9E536aB0b6c721e11f383C326F2Ad1D5', + 'mock sandbox governance': '0x1677d9cc4861f1c85ac7009d5f06f49c928ca2ad', } }; @@ -11,10 +24,103 @@ const collectAbi = [ "function collect(uint256 profileId, uint256 pubId, bytes calldata data) external returns (uint256)" ]; -export const getLensHub = async (provider: any, networkName: string = 'mumbai') => ( - new Contract(lensAddresses[networkName]['lensHub proxy'], collectAbi, provider) +const whitelistAbi = [ + 'function whitelistFollowModule(address followModule, bool whitelist)', + 'function whitelistReferenceModule(address referenceModule, bool whitelist)', + 'function whitelistCollectModule(address collectModule, bool whitelist)' +]; + +export const getLensHub = (key: string = 'lensHub proxy', provider: any, networkName: string = 'mumbai') => ( + new Contract(lensAddresses[networkName][key], collectAbi, provider) +); + +export const getLensHubDeployed = ( + key: string = 'lensHub proxy', + networkName: string = 'mumbai', + provider: any, +) => ( + new Contract(lensAddresses[networkName][key], LensHub.abi, provider) +); + +export const getMockSandboxGovernance = (provider: any) => ( + new Contract(lensAddresses['mumbai']['mock sandbox governance'], whitelistAbi, provider) ); +type FollowWithSigDataProps = { + chainId: number; + wallet: any; // Signer + lensHubAddress: string; + profileIds: BigNumber[] | string[]; + datas: Bytes[] | string[]; + nonce: number; + deadline: string; + follower: string; +}; + +const buildFollowWithSigParams = ( + chainId: number, + lensHubAddress: string, + profileIds: BigNumber[] | string[], + datas: Bytes[] | string[], + nonce: number, + deadline: string +) => ({ + types: { + FollowWithSig: [ + { name: 'profileIds', type: 'uint256[]' }, + { name: 'datas', type: 'bytes[]' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + domain: { + name: LENS_DOMAIN_NAME, + version: LENS_DOMAIN_VERSION, + chainId, + verifyingContract: lensHubAddress, + }, + value: { + profileIds: profileIds, + datas: datas, + nonce: nonce, + deadline: deadline, + }, +}); + +export const getFollowWithSigParts = async ({ + chainId, + wallet, + lensHubAddress, + profileIds, + datas, + nonce, + deadline, + follower +}: FollowWithSigDataProps) => { + const msgParams = buildFollowWithSigParams( + chainId, + lensHubAddress, + profileIds, + datas, + nonce, + deadline + ); + const sig = await wallet._signTypedData(msgParams.domain, msgParams.types, msgParams.value); + const { v, r, s } = utils.splitSignature(sig); + + return { + follower, + profileIds, + datas, + sig: { + v, + r, + s, + deadline, + }, + }; +} + // testnet accounts export const accounts = [ { secretKey: '0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122' }, diff --git a/tasks/stub-collect-post.ts b/tasks/stub-collect-post.ts index f029ea5..5cb8238 100644 --- a/tasks/stub-collect-post.ts +++ b/tasks/stub-collect-post.ts @@ -8,7 +8,7 @@ task('stub-collect-post', 'collects our lens post').setAction(async ({}, hre) => const ethers = hre.ethers; const networkName = hre.network.name; const [_, governance] = await ethers.getSigners(); // uses priv key defined in this hardhat config - const lensHub = await getLensHub(governance.provider); + const lensHub = await getLensHub('lensHub proxy', governance.provider); const { maxFeePerGas, maxPriorityFeePerGas } = await lensHub.provider.getFeeData(); diff --git a/test/LZCollectModule.spec.ts b/test/LZCollectModule.spec.ts index 7ee3976..928e7ca 100644 --- a/test/LZCollectModule.spec.ts +++ b/test/LZCollectModule.spec.ts @@ -27,25 +27,13 @@ import { } from '../typechain-types' import { abi } from './lens/abi/Events.json'; import LZCollectModuleABI from './../build/contracts/LZCollectModule.sol/LZCollectModule.json'; +import { parseLogsNested } from './utils/parseLogs'; const EMPTY_BYTES = '0x'; const FIRST_PUB_ID = 1; const FIRST_COLLECTION_ID = 1; const CHAIN_ID = 123; -// HACKY: attempt to parse logs from Lens=>Events lib and LZCollectModule -const parseLogs = ({ logs }) => { - const iface = new ethers.utils.Interface(abi); - const followIFace = new ethers.utils.Interface(LZCollectModuleABI.abi); - return (logs.map((log) => { - try { return iface.parseLog(log); } - catch { - try { return followIFace.parseLog(log); } - catch {} - } - })).filter((l) => l); -}; - makeSuiteCleanRoom('LZCollectModule', function () { let omniNFTSource: OmniSBT, omniNFTDestination: OmniSBT; let lzEndpoint: LZEndpointMock @@ -153,7 +141,7 @@ makeSuiteCleanRoom('LZCollectModule', function () { }); it('emits an event', async () => { - const logs = parseLogs(await tx.wait()); + const logs = parseLogsNested(await tx.wait(), abi, LZCollectModuleABI.abi); const event = logs.find(({ name }) => name === 'InitCollectModule'); expect(event).not.to.equal(undefined); expect(event.args.collectionId.toNumber()).to.equal(FIRST_COLLECTION_ID); diff --git a/test/LZGatedCollectModule.spec.ts b/test/LZGatedCollectModule.spec.ts new file mode 100644 index 0000000..8305db4 --- /dev/null +++ b/test/LZGatedCollectModule.spec.ts @@ -0,0 +1,340 @@ +import '@nomiclabs/hardhat-ethers'; +import { expect } from 'chai'; +import { Signer, VoidSigner } from 'ethers'; +const { getContractAddress } = require('@ethersproject/address'); +import { ethers } from 'hardhat'; +import { + FIRST_PROFILE_ID, + MOCK_URI, + OTHER_MOCK_URI, + governance, + lensHub, + makeSuiteCleanRoom, + MOCK_FOLLOW_NFT_URI, + MOCK_PROFILE_HANDLE, + MOCK_PROFILE_URI, + user, + userTwo, + userThree, + testWallet, + freeCollectModule, +} from './lens/__setup.spec'; +import { ERRORS } from './lens/helpers/errors'; +import { getCollectWithSigParts } from './lens/helpers/utils'; +import { ZERO_ADDRESS, MAX_UINT256 } from './lens/helpers/constants'; +import { + LZGatedCollectModule, + LZGatedCollectModule__factory, + LZGatedProxy, + LZGatedProxy__factory, + LZEndpointMock, + LZEndpointMock__factory, + ERC721Mock, + ERC721Mock__factory, + ERC20Mock, + ERC20Mock__factory, +} from '../typechain-types' +import { abi } from './lens/abi/Events.json'; +import LZGatedCollectModuleABI from './../build/contracts/LZGatedCollectModule.sol/LZGatedCollectModule.json'; +import { parseLogsNested } from './utils/parseLogs'; + +const EMPTY_BYTES = '0x'; +const REMOTE_CHAIN_ID = 123; +const BALANCE_THRESHOLD = 1; + +makeSuiteCleanRoom('LZGatedCollectModule', function () { + let lzGatedProxy: LZGatedProxy; + let lzEndpoint: LZEndpointMock; + let collectModule: LZGatedCollectModule; + let erc721: ERC721Mock; + let erc20: ERC20Mock; + let deployer: Signer, user: Signer, userTwo: Signer, userThree: Signer; + let deployerAddress: string, userAddress: string, userTwoAddress: string; + + const getNonBlockingError = (receipt) => { + const logs = parseLogsNested(receipt, abi, LZGatedCollectModuleABI.abi); + const event = logs.find(({ name }) => name === 'MessageFailed'); + + return event.args._reason; + }; + + beforeEach(async () => { + ([deployer, user, userTwo, userThree] = await ethers.getSigners()); + deployerAddress = await deployer.getAddress(); + userAddress = await user.getAddress(); + userTwoAddress = await userTwo.getAddress(); + + lzEndpoint = await new LZEndpointMock__factory(deployer).deploy(REMOTE_CHAIN_ID); + const transactionCount = await deployer.getTransactionCount(); + const collectModuleAddress = getContractAddress({ from: deployerAddress, nonce: transactionCount + 1 }); + + lzGatedProxy = await new LZGatedProxy__factory(deployer).deploy( + lzEndpoint.address, + REMOTE_CHAIN_ID, + ZERO_ADDRESS, // _remoteFollowModule + ZERO_ADDRESS, // _remoteReferenceModule + collectModuleAddress + ); + collectModule = await new LZGatedCollectModule__factory(deployer).deploy( + lensHub.address, + lzEndpoint.address, + [REMOTE_CHAIN_ID], + [lzGatedProxy.address] + ); + erc721 = await new ERC721Mock__factory(deployer).deploy(); + erc20 = await new ERC20Mock__factory(deployer).deploy(); + + // use same lz endpoint mock + await lzEndpoint.setDestLzEndpoint(collectModule.address, lzEndpoint.address); + await lzEndpoint.setDestLzEndpoint(lzGatedProxy.address, lzEndpoint.address); + + await lensHub.connect(governance).whitelistCollectModule(collectModule.address, true); + + await lensHub.createProfile({ + to: userAddress, + handle: MOCK_PROFILE_HANDLE, + imageURI: MOCK_PROFILE_URI, + followModule: ZERO_ADDRESS, + followModuleInitData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }); + + await lensHub.createProfile({ + to: userTwoAddress, + handle: 'test.lens', + imageURI: MOCK_PROFILE_URI, + followModule: ZERO_ADDRESS, + followModuleInitData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }); + }); + + describe('#constructor', () => { + it('reverts when the hub arg is the null address', async () => { + expect( + new LZGatedCollectModule__factory(deployer).deploy(ZERO_ADDRESS, lzEndpoint.address, [], []) + ).to.be.revertedWith('InitParamsInvalid'); + }); + + it('sets storage', async () => { + const owner = await collectModule.owner(); + const endpoint = await collectModule.lzEndpoint(); + + expect(owner).to.equal(deployerAddress); + expect(endpoint).to.equal(lzEndpoint.address); + }); + }); + + describe('#initializePublicationCollectModule', () => { + it('reverts when the caller is not LensHub', async () => { + await expect( + collectModule.initializePublicationCollectModule(FIRST_PROFILE_ID, 1, EMPTY_BYTES) + ).to.be.revertedWith(ERRORS.NOT_HUB); + }); + + it('reverts when an invalid chain id is provided in the encoded data', async () => { + const collectModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [erc721.address, BALANCE_THRESHOLD, 12345] + ); + + await expect( + lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: collectModule.address, + collectModuleInitData, + referenceModule: ZERO_ADDRESS, + referenceModuleInitData: [], + }) + ).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID); + }); + + it('reverts when token contract as zero address is provided in the encoded data', async () => { + const collectModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [ZERO_ADDRESS, BALANCE_THRESHOLD, REMOTE_CHAIN_ID] + ); + + await expect( + lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: collectModule.address, + collectModuleInitData, + referenceModule: ZERO_ADDRESS, + referenceModuleInitData: [], + }) + ).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID); + }); + + context('context: with valid params', () => { + let tx; + + beforeEach(async() => { + const collectModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [erc721.address, BALANCE_THRESHOLD, REMOTE_CHAIN_ID] + ); + tx = await lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: collectModule.address, + collectModuleInitData, + referenceModule: ZERO_ADDRESS, + referenceModuleInitData: [], + }); + }); + + it('sets storage', async () => { + const res = await collectModule.gatedCollectDataPerPub(FIRST_PROFILE_ID, 1); + + expect(res.balanceThreshold.toNumber()).to.equal(BALANCE_THRESHOLD); + expect(res.tokenContract).to.equal(erc721.address); + expect(res.remoteChainId).to.equal(REMOTE_CHAIN_ID); + }); + + it('emits an event', async () => { + const logs = parseLogsNested(await tx.wait(), abi, LZGatedCollectModuleABI.abi); + const event = logs.find(({ name }) => name === 'InitCollectModule'); + + expect(event).not.to.equal(undefined); + expect(event.args.profileId.toNumber()).to.equal(FIRST_PROFILE_ID); + expect(event.args.tokenContract).to.equal(erc721.address); + expect(event.args.chainId).to.equal(REMOTE_CHAIN_ID); + }); + }); + }); + + describe('#processCollect (triggered from LZGatedProxy#relayCollectWithSig)', () => { + let collectWithSigData; + let collectModuleInitData; + + beforeEach(async() => { + collectModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [erc721.address, BALANCE_THRESHOLD, REMOTE_CHAIN_ID] + ); + + await lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: collectModule.address, + collectModuleInitData, + referenceModule: ZERO_ADDRESS, + referenceModuleInitData: [], + }); + + const nonce = (await lensHub.sigNonces(userTwoAddress)).toNumber(); + const { v, r, s } = await getCollectWithSigParts( + userTwo as VoidSigner, + FIRST_PROFILE_ID, + '1', + [], + nonce, + MAX_UINT256 + ); + + // userTwo signs that they would like to collect user's first post + collectWithSigData = { + collector: userTwoAddress, + profileId: FIRST_PROFILE_ID, + pubId: '1', + data: [], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }; + }); + + it('reverts if called without going through lzGatedProxy', async () => { + await expect( + lensHub.collectWithSig(collectWithSigData) + ).to.be.revertedWith('CollectNotAllowed()'); + }); + + it('reverts if the caller does not have suffient balance', async () => { + await expect( + lzGatedProxy + .connect(userTwo) + .relayCollectWithSig( + erc721.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + collectWithSigData + ) + ).to.be.revertedWith('InsufficientBalance'); + }); + + it('reverts if the contract call for balanceOf() fails', async () => { + await expect( + lzGatedProxy + .connect(userTwo) + .relayCollectWithSig( + lzEndpoint.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + collectWithSigData + ) + ).to.be.revertedWith('InsufficientBalance'); + }); + + it('[non-blocking] fails if the caller passed an invalid threshold', async () => { + await erc721.safeMint(userTwoAddress); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayCollectWithSig( + erc721.address, + 0, + 0, // lzCustomGasAmount + collectWithSigData + ); + + const messageFailedReason = getNonBlockingError(await tx.wait()); + expect(messageFailedReason).to.equal('InvalidRemoteInput'); + }); + + + it('[non-blocking] fails if the caller passed an invalid token contract', async () => { + await erc20.mint(userTwoAddress, BALANCE_THRESHOLD); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayCollectWithSig( + erc20.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + collectWithSigData + ); + + const messageFailedReason = getNonBlockingError(await tx.wait()); + expect(messageFailedReason).to.equal('InvalidRemoteInput'); + }); + + it('processes a valid collect', async () => { + await erc721.safeMint(userTwoAddress); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayCollectWithSig( + erc721.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + collectWithSigData + ); + const logs = parseLogsNested(await tx.wait(), abi, LZGatedCollectModuleABI.abi); + + const event = logs.find(({ name }) => name === 'MessageFailed'); + const collectEvent = logs.find(({ name }) => name === 'Collected'); + + expect(event).to.equal(undefined); + expect(collectEvent).not.to.equal(undefined); + expect(collectEvent.args.rootPubId.toNumber()).to.equal(1); + expect(collectEvent.args.rootProfileId.toNumber()).to.equal(1); + }); + }); +}); diff --git a/test/LZGatedFollowModule.spec.ts b/test/LZGatedFollowModule.spec.ts new file mode 100644 index 0000000..03873f4 --- /dev/null +++ b/test/LZGatedFollowModule.spec.ts @@ -0,0 +1,337 @@ +import '@nomiclabs/hardhat-ethers'; +import { expect } from 'chai'; +import { Signer, VoidSigner } from 'ethers'; +const { getContractAddress } = require('@ethersproject/address'); +import { ethers } from 'hardhat'; +import { + FIRST_PROFILE_ID, + MOCK_URI, + governance, + lensHub, + makeSuiteCleanRoom, + MOCK_FOLLOW_NFT_URI, + MOCK_PROFILE_HANDLE, + MOCK_PROFILE_URI, + user, + userTwo, + userThree, + testWallet, +} from './lens/__setup.spec'; +import { ERRORS } from './lens/helpers/errors'; +import { getFollowWithSigParts } from './lens/helpers/utils'; +import { ZERO_ADDRESS, MAX_UINT256 } from './lens/helpers/constants'; +import { + LZGatedFollowModule, + LZGatedFollowModule__factory, + LZGatedProxy, + LZGatedProxy__factory, + LZEndpointMock, + LZEndpointMock__factory, + ERC721Mock, + ERC721Mock__factory, + ERC20Mock, + ERC20Mock__factory, +} from '../typechain-types' +import { abi } from './lens/abi/Events.json'; +import { FollowNFT__factory } from '../typechain-types-lens'; +import LZGatedFollowModuleABI from './../build/contracts/LZGatedFollowModule.sol/LZGatedFollowModule.json'; +import { parseLogsNested } from './utils/parseLogs'; + +const EMPTY_BYTES = '0x'; +const REMOTE_CHAIN_ID = 123; +const BALANCE_THRESHOLD = 1; + +makeSuiteCleanRoom('LZGatedFollowModule', function () { + let lzGatedProxy: LZGatedProxy; + let lzEndpoint: LZEndpointMock; + let followModule: LZGatedFollowModule; + let erc721: ERC721Mock; + let erc20: ERC20Mock; + let deployer: Signer, user: Signer, userTwo: Signer, userThree: Signer; + let deployerAddress: string, userAddress: string, userTwoAddress: string; + + const setFollowModule = async ({ + tokenContract = erc721.address, + tokenThreshold = BALANCE_THRESHOLD, + chainId = REMOTE_CHAIN_ID + }) => { + const followModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [tokenContract, tokenThreshold, chainId] + ); + + return await lensHub.setFollowModule(FIRST_PROFILE_ID, followModule.address, followModuleInitData); + }; + + const getNonBlockingError = (receipt) => { + const logs = parseLogsNested(receipt, abi, LZGatedFollowModuleABI.abi); + const event = logs.find(({ name }) => name === 'MessageFailed'); + + return event.args._reason; + }; + + beforeEach(async () => { + ([deployer, user, userTwo, userThree] = await ethers.getSigners()); + deployerAddress = await deployer.getAddress(); + userAddress = await user.getAddress(); + userTwoAddress = await userTwo.getAddress(); + + lzEndpoint = await new LZEndpointMock__factory(deployer).deploy(REMOTE_CHAIN_ID); + const transactionCount = await deployer.getTransactionCount(); + const followModuleAddress = getContractAddress({ from: deployerAddress, nonce: transactionCount + 1 }); + + lzGatedProxy = await new LZGatedProxy__factory(deployer).deploy( + lzEndpoint.address, + REMOTE_CHAIN_ID, + followModuleAddress, + ZERO_ADDRESS, // _remoteReferenceModule + ZERO_ADDRESS // _remoteCollectModule + ); + followModule = await new LZGatedFollowModule__factory(deployer).deploy( + lensHub.address, + lzEndpoint.address, + [REMOTE_CHAIN_ID], + [lzGatedProxy.address] + ); + erc721 = await new ERC721Mock__factory(deployer).deploy(); + erc20 = await new ERC20Mock__factory(deployer).deploy(); + + // use same lz endpoint mock + await lzEndpoint.setDestLzEndpoint(followModule.address, lzEndpoint.address); + await lzEndpoint.setDestLzEndpoint(lzGatedProxy.address, lzEndpoint.address); + + await lensHub.connect(governance).whitelistFollowModule(followModule.address, true); + + await lensHub.createProfile({ + to: userAddress, + handle: MOCK_PROFILE_HANDLE, + imageURI: MOCK_PROFILE_URI, + followModule: ZERO_ADDRESS, + followModuleInitData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }); + }); + + describe('#constructor', () => { + it('reverts when the hub arg is the null address', async () => { + expect( + new LZGatedFollowModule__factory(deployer).deploy(ZERO_ADDRESS, lzEndpoint.address, [], []) + ).to.be.revertedWith('InitParamsInvalid'); + }); + + it('sets storage', async () => { + const owner = await followModule.owner(); + const endpoint = await followModule.lzEndpoint(); + + expect(owner).to.equal(deployerAddress); + expect(endpoint).to.equal(lzEndpoint.address); + }); + }); + + describe('#initializeFollowModule', () => { + it('reverts when the caller is not LensHub', async () => { + await expect( + followModule.initializeFollowModule(FIRST_PROFILE_ID, EMPTY_BYTES) + ).to.be.revertedWith(ERRORS.NOT_HUB); + }); + + it('reverts when an invalid chain id is provided in the encoded data', async () => { + const followModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [erc721.address, BALANCE_THRESHOLD, 12345] + ); + + await expect( + lensHub.setFollowModule(FIRST_PROFILE_ID, followModule.address, followModuleInitData) + ).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID); + }); + + it('reverts when token contract as zero address is provided in the encoded data', async () => { + const followModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [ZERO_ADDRESS, BALANCE_THRESHOLD, REMOTE_CHAIN_ID] + ); + + await expect( + lensHub.setFollowModule(FIRST_PROFILE_ID, followModule.address, followModuleInitData) + ).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID); + }); + + context('context: with valid params', () => { + let tx; + + beforeEach(async() => { + tx = await setFollowModule({ + tokenContract: erc721.address, + tokenThreshold: BALANCE_THRESHOLD, + chainId: REMOTE_CHAIN_ID + }); + }); + + it('sets storage', async () => { + const res = await followModule.gatedFollowPerProfile(FIRST_PROFILE_ID); + + expect(res.balanceThreshold.toNumber()).to.equal(BALANCE_THRESHOLD); + expect(res.tokenContract).to.equal(erc721.address); + expect(res.remoteChainId).to.equal(REMOTE_CHAIN_ID); + }); + + it('emits an event', async () => { + const logs = parseLogsNested(await tx.wait(), abi, LZGatedFollowModuleABI.abi); + const event = logs.find(({ name }) => name === 'InitFollowModule'); + + expect(event).not.to.equal(undefined); + expect(event.args.profileId.toNumber()).to.equal(FIRST_PROFILE_ID); + expect(event.args.tokenContract).to.equal(erc721.address); + expect(event.args.chainId).to.equal(REMOTE_CHAIN_ID); + }); + }); + }); + + describe('#processFollow (triggered from LZGatedProxy#relayFollowWithSig)', () => { + let followWithSigData; + + beforeEach(async() => { + await setFollowModule({ + tokenContract: erc721.address, + tokenThreshold: BALANCE_THRESHOLD, + chainId: REMOTE_CHAIN_ID + }); + + const nonce = (await lensHub.sigNonces(userAddress)).toNumber(); + const { v, r, s } = await getFollowWithSigParts( + user as VoidSigner, + [FIRST_PROFILE_ID], + [[]], + nonce, + MAX_UINT256 + ); + + followWithSigData = { + follower: userAddress, + profileIds: [FIRST_PROFILE_ID], + datas: [[]], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }; + }); + + it('reverts if called without going through lzGatedProxy', async () => { + await expect( + lensHub.followWithSig(followWithSigData) + ).to.be.revertedWith(ERRORS.FOLLOW_INVALID); + }); + + it('reverts if the caller does not have suffient balance', async () => { + await expect( + lzGatedProxy + .connect(user) + .relayFollowWithSig( + erc721.address, + BALANCE_THRESHOLD, + 0, // customGasAmount + followWithSigData + ) + ).to.be.revertedWith('InsufficientBalance'); + }); + + it('reverts if the contract call for balanceOf() fails', async () => { + await expect( + lzGatedProxy + .connect(user) + .relayFollowWithSig( + lzEndpoint.address, + BALANCE_THRESHOLD, + 0, // customGasAmount + followWithSigData + ) + ).to.be.revertedWith('InsufficientBalance'); + }); + + it('[non-blocking] fails if the caller passed an invalid threshold', async () => { + await erc721.safeMint(userAddress); + + const tx = await lzGatedProxy + .connect(user) + .relayFollowWithSig( + erc721.address, + 0, + 0, // customGasAmount + followWithSigData + ); + + const messageFailedReason = getNonBlockingError(await tx.wait()); + expect(messageFailedReason).to.equal('InvalidRemoteInput'); + }); + + + it('[non-blocking] fails if the caller passed an invalid token contract', async () => { + await erc20.mint(userAddress, BALANCE_THRESHOLD); + + const tx = await lzGatedProxy + .connect(user) + .relayFollowWithSig( + erc20.address, + BALANCE_THRESHOLD, + 0, // customGasAmount + followWithSigData + ); + + const messageFailedReason = getNonBlockingError(await tx.wait()); + expect(messageFailedReason).to.equal('InvalidRemoteInput'); + }); + + it('processes a valid follow', async () => { + await erc721.safeMint(userAddress); + + const tx = await lzGatedProxy + .connect(user) + .relayFollowWithSig( + erc721.address, + BALANCE_THRESHOLD, + 0, // customGasAmount + followWithSigData, + ); + const logs = parseLogsNested(await tx.wait(), abi, LZGatedFollowModuleABI.abi); + const event = logs.find(({ name }) => name === 'MessageFailed'); + + expect(event).to.equal(undefined); + + const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID); + const followNFT = FollowNFT__factory.connect(followNFTAddress, user); + const id = await followNFT.tokenOfOwnerByIndex(userAddress, 0); + expect(id).to.eq(1); + }); + + /** + * NOTE: it reverts here in the test because we mocked lzEndpoint; it should actually just fail (emit error) + */ + it('reverts if the same signature is used', async () => { + await erc721.safeMint(userAddress); + + await lzGatedProxy + .connect(user) + .relayFollowWithSig( + erc721.address, + BALANCE_THRESHOLD, + 0, // customGasAmount + followWithSigData + ); + + await expect( + lzGatedProxy + .connect(user) + .relayFollowWithSig( + erc721.address, + BALANCE_THRESHOLD, + 0, // customGasAmount + followWithSigData + ) + ).to.be.reverted; // pretty sure the revert is sig-related but no reason is provided + }); + }); +}); diff --git a/test/LZGatedProxy.spec.ts b/test/LZGatedProxy.spec.ts new file mode 100644 index 0000000..ea93d72 --- /dev/null +++ b/test/LZGatedProxy.spec.ts @@ -0,0 +1,112 @@ +import '@nomiclabs/hardhat-ethers'; +import { expect } from 'chai'; +import { Signer, utils } from 'ethers'; +const { getContractAddress } = require('@ethersproject/address'); +import { ethers } from 'hardhat'; +import { ZERO_ADDRESS } from './lens/helpers/constants'; +import { + LZGatedFollowModule, + LZGatedFollowModule__factory, + LZGatedReferenceModule, + LZGatedReferenceModule__factory, + LZGatedCollectModule, + LZGatedCollectModule__factory, + LZGatedProxy, + LZGatedProxy__factory, + LZEndpointMock, + LZEndpointMock__factory, +} from '../typechain-types' + +const REMOTE_CHAIN_ID = 123; +const CONFIG_TYPE_INBOUND_PROOF_LIBRARY_VERSION = 1; + +describe('LZGatedProxy', () => { + let lzGatedProxy: LZGatedProxy; + let lzEndpoint: LZEndpointMock; + let followModule: LZGatedFollowModule; + let referenceModule: LZGatedReferenceModule; + let collectModule: LZGatedCollectModule; + let deployer: Signer, user: Signer, userTwo: Signer, userThree: Signer; + let deployerAddress: string, userAddress: string, userTwoAddress: string; + + beforeEach(async () => { + ([deployer, user, userTwo, userThree] = await ethers.getSigners()); + deployerAddress = await deployer.getAddress(); + userAddress = await user.getAddress(); + userTwoAddress = await userTwo.getAddress(); + + lzEndpoint = await new LZEndpointMock__factory(deployer).deploy(REMOTE_CHAIN_ID); + const transactionCount = await deployer.getTransactionCount(); + const followModuleAddress = getContractAddress({ from: deployerAddress, nonce: transactionCount + 1 }); + const referenceModuleAddress = getContractAddress({ from: deployerAddress, nonce: transactionCount + 2 }); + const collectModuleAddress = getContractAddress({ from: deployerAddress, nonce: transactionCount + 3 }); + + lzGatedProxy = await new LZGatedProxy__factory(deployer).deploy( + lzEndpoint.address, + REMOTE_CHAIN_ID, + followModuleAddress, + referenceModuleAddress, + collectModuleAddress + ); + followModule = await new LZGatedFollowModule__factory(deployer).deploy( + lzGatedProxy.address, // lensHub does not matter + lzEndpoint.address, + [REMOTE_CHAIN_ID], + [lzGatedProxy.address] + ); + referenceModule = await new LZGatedReferenceModule__factory(deployer).deploy( + lzGatedProxy.address, // lensHub does not matter + lzEndpoint.address, + [REMOTE_CHAIN_ID], + [lzGatedProxy.address] + ); + collectModule = await new LZGatedCollectModule__factory(deployer).deploy( + lzGatedProxy.address, // lensHub does not matter + lzEndpoint.address, + [REMOTE_CHAIN_ID], + [lzGatedProxy.address] + ); + + // use same lz endpoint mock + await lzEndpoint.setDestLzEndpoint(referenceModule.address, lzEndpoint.address); + await lzEndpoint.setDestLzEndpoint(followModule.address, lzEndpoint.address); + await lzEndpoint.setDestLzEndpoint(collectModule.address, lzEndpoint.address); + await lzEndpoint.setDestLzEndpoint(lzGatedProxy.address, lzEndpoint.address); + }); + + describe('#constructor', () => { + it('reverts when the _lzEndpoint arg is the null address', async () => { + expect( + new LZGatedProxy__factory(deployer).deploy( + ZERO_ADDRESS, + REMOTE_CHAIN_ID, + followModule.address, + referenceModule.address, + ZERO_ADDRESS + ) + ).to.be.revertedWith('NotZeroAddress'); + }); + + it('sets storage', async () => { + const owner = await lzGatedProxy.owner(); + const endpoint = await lzGatedProxy.lzEndpoint(); + const followModuleAddress = await lzGatedProxy.remoteFollowModule(); + const remoteReferenceModule = await lzGatedProxy.remoteReferenceModule(); + const remoteCollectModule = await lzGatedProxy.remoteCollectModule(); + + expect(owner).to.equal(deployerAddress); + expect(endpoint).to.equal(lzEndpoint.address); + expect(utils.getAddress(followModuleAddress)).to.equal(followModule.address); + expect(utils.getAddress(remoteReferenceModule)).to.equal(referenceModule.address); + expect(utils.getAddress(remoteCollectModule)).to.equal(collectModule.address); + }); + }); + + describe.skip('#relayFollowWithSig (part of LZGatedFollowModule.spec.ts)', () => {}); + + describe.skip('#relayCommentWithSig (part of LZGatedReferenceModule.spec.ts)', () => {}); + + describe.skip('#relayMirrorWithSig (part of LZGatedReferenceModule.spec.ts)', () => {}); + + describe.skip('#relayCollectWithSig (part of LZGatedCollectModule.spec.ts)', () => {}); +}); diff --git a/test/LZGatedReferenceModule.spec.ts b/test/LZGatedReferenceModule.spec.ts new file mode 100644 index 0000000..7f77f08 --- /dev/null +++ b/test/LZGatedReferenceModule.spec.ts @@ -0,0 +1,501 @@ +import '@nomiclabs/hardhat-ethers'; +import { expect } from 'chai'; +import { Signer, VoidSigner } from 'ethers'; +const { getContractAddress } = require('@ethersproject/address'); +import { ethers } from 'hardhat'; +import { + FIRST_PROFILE_ID, + MOCK_URI, + OTHER_MOCK_URI, + governance, + lensHub, + makeSuiteCleanRoom, + MOCK_FOLLOW_NFT_URI, + MOCK_PROFILE_HANDLE, + MOCK_PROFILE_URI, + user, + userTwo, + userThree, + testWallet, + freeCollectModule, +} from './lens/__setup.spec'; +import { ERRORS } from './lens/helpers/errors'; +import { getCommentWithSigParts, getMirrorWithSigParts } from './lens/helpers/utils'; +import { ZERO_ADDRESS, MAX_UINT256 } from './lens/helpers/constants'; +import { + LZGatedReferenceModule, + LZGatedReferenceModule__factory, + LZGatedProxy, + LZGatedProxy__factory, + LZEndpointMock, + LZEndpointMock__factory, + ERC721Mock, + ERC721Mock__factory, + ERC20Mock, + ERC20Mock__factory, +} from '../typechain-types' +import { abi } from './lens/abi/Events.json'; +import LZGatedReferenceModuleABI from './../build/contracts/LZGatedReferenceModule.sol/LZGatedReferenceModule.json'; +import { parseLogsNested } from './utils/parseLogs'; + +const EMPTY_BYTES = '0x'; +const REMOTE_CHAIN_ID = 123; +const BALANCE_THRESHOLD = 1; + +makeSuiteCleanRoom('LZGatedReferenceModule', function () { + let lzGatedProxy: LZGatedProxy; + let lzEndpoint: LZEndpointMock; + let referenceModule: LZGatedReferenceModule; + let erc721: ERC721Mock; + let erc20: ERC20Mock; + let deployer: Signer, user: Signer, userTwo: Signer, userThree: Signer; + let deployerAddress: string, userAddress: string, userTwoAddress: string; + + const getNonBlockingError = (receipt) => { + const logs = parseLogsNested(receipt, abi, LZGatedReferenceModuleABI.abi); + const event = logs.find(({ name }) => name === 'MessageFailed'); + + return event.args._reason; + }; + + beforeEach(async () => { + ([deployer, user, userTwo, userThree] = await ethers.getSigners()); + deployerAddress = await deployer.getAddress(); + userAddress = await user.getAddress(); + userTwoAddress = await userTwo.getAddress(); + + lzEndpoint = await new LZEndpointMock__factory(deployer).deploy(REMOTE_CHAIN_ID); + const transactionCount = await deployer.getTransactionCount(); + const referenceModuleAddress = getContractAddress({ from: deployerAddress, nonce: transactionCount + 1 }); + + lzGatedProxy = await new LZGatedProxy__factory(deployer).deploy( + lzEndpoint.address, + REMOTE_CHAIN_ID, + ZERO_ADDRESS, // _remoteFollowModule + referenceModuleAddress, + ZERO_ADDRESS // _remoteCollectModule + ); + referenceModule = await new LZGatedReferenceModule__factory(deployer).deploy( + lensHub.address, + lzEndpoint.address, + [REMOTE_CHAIN_ID], + [lzGatedProxy.address] + ); + erc721 = await new ERC721Mock__factory(deployer).deploy(); + erc20 = await new ERC20Mock__factory(deployer).deploy(); + + // use same lz endpoint mock + await lzEndpoint.setDestLzEndpoint(referenceModule.address, lzEndpoint.address); + await lzEndpoint.setDestLzEndpoint(lzGatedProxy.address, lzEndpoint.address); + + await lensHub.connect(governance).whitelistCollectModule(freeCollectModule.address, true) + await lensHub.connect(governance).whitelistReferenceModule(referenceModule.address, true); + + await lensHub.createProfile({ + to: userAddress, + handle: MOCK_PROFILE_HANDLE, + imageURI: MOCK_PROFILE_URI, + followModule: ZERO_ADDRESS, + followModuleInitData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }); + + await lensHub.createProfile({ + to: userTwoAddress, + handle: 'test.lens', + imageURI: MOCK_PROFILE_URI, + followModule: ZERO_ADDRESS, + followModuleInitData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }); + }); + + describe('#constructor', () => { + it('reverts when the hub arg is the null address', async () => { + expect( + new LZGatedReferenceModule__factory(deployer).deploy(ZERO_ADDRESS, lzEndpoint.address, [], []) + ).to.be.revertedWith('InitParamsInvalid'); + }); + + it('sets storage', async () => { + const owner = await referenceModule.owner(); + const endpoint = await referenceModule.lzEndpoint(); + + expect(owner).to.equal(deployerAddress); + expect(endpoint).to.equal(lzEndpoint.address); + }); + }); + + describe('#initializeReferenceModule', () => { + it('reverts when the caller is not LensHub', async () => { + await expect( + referenceModule.initializeReferenceModule(FIRST_PROFILE_ID, 1, EMPTY_BYTES) + ).to.be.revertedWith(ERRORS.NOT_HUB); + }); + + it('reverts when an invalid chain id is provided in the encoded data', async () => { + const referenceModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [erc721.address, BALANCE_THRESHOLD, 12345] + ); + + await expect( + lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: freeCollectModule.address, + collectModuleInitData: ethers.utils.defaultAbiCoder.encode(['bool'], [true]), + referenceModule: referenceModule.address, + referenceModuleInitData: referenceModuleInitData, + }) + ).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID); + }); + + it('reverts when token contract as zero address is provided in the encoded data', async () => { + const referenceModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [ZERO_ADDRESS, BALANCE_THRESHOLD, REMOTE_CHAIN_ID] + ); + + await expect( + lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: freeCollectModule.address, + collectModuleInitData: ethers.utils.defaultAbiCoder.encode(['bool'], [true]), + referenceModule: referenceModule.address, + referenceModuleInitData: referenceModuleInitData, + }) + ).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID); + }); + + context('context: with valid params', () => { + let tx; + + beforeEach(async() => { + const referenceModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [erc721.address, BALANCE_THRESHOLD, REMOTE_CHAIN_ID] + ); + tx = await lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: freeCollectModule.address, + collectModuleInitData: ethers.utils.defaultAbiCoder.encode(['bool'], [true]), + referenceModule: referenceModule.address, + referenceModuleInitData: referenceModuleInitData, + }); + }); + + it('sets storage', async () => { + const res = await referenceModule.gatedReferenceDataPerPub(FIRST_PROFILE_ID, 1); + + expect(res.balanceThreshold.toNumber()).to.equal(BALANCE_THRESHOLD); + expect(res.tokenContract).to.equal(erc721.address); + expect(res.remoteChainId).to.equal(REMOTE_CHAIN_ID); + }); + + it('emits an event', async () => { + const logs = parseLogsNested(await tx.wait(), abi, LZGatedReferenceModuleABI.abi); + const event = logs.find(({ name }) => name === 'InitReferenceModule'); + + expect(event).not.to.equal(undefined); + expect(event.args.profileId.toNumber()).to.equal(FIRST_PROFILE_ID); + expect(event.args.tokenContract).to.equal(erc721.address); + expect(event.args.chainId).to.equal(REMOTE_CHAIN_ID); + }); + }); + }); + + describe('#processComment (triggered from LZGatedProxy#relayCommentWithSig)', () => { + let commentWithSigData; + let referenceModuleInitData; + + beforeEach(async() => { + const collectModuleInitData = ethers.utils.defaultAbiCoder.encode(['bool'], [true]); + referenceModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [erc721.address, BALANCE_THRESHOLD, REMOTE_CHAIN_ID] + ); + + await lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: freeCollectModule.address, + collectModuleInitData, + referenceModule: referenceModule.address, + referenceModuleInitData: referenceModuleInitData, + }); + + const nonce = (await lensHub.sigNonces(userTwoAddress)).toNumber(); + const { v, r, s } = await getCommentWithSigParts( + userTwo as VoidSigner, + FIRST_PROFILE_ID + 1, + OTHER_MOCK_URI, + FIRST_PROFILE_ID, + '1', + [], + freeCollectModule.address, + collectModuleInitData, + ZERO_ADDRESS, + [], + nonce, + MAX_UINT256 + ); + + // userTwo signs that they would like to comment on user's first post + commentWithSigData = { + profileId: FIRST_PROFILE_ID + 1, + contentURI: OTHER_MOCK_URI, + profileIdPointed: FIRST_PROFILE_ID, + pubIdPointed: '1', + collectModule: freeCollectModule.address, + collectModuleInitData, + referenceModuleData: [], + referenceModule: ZERO_ADDRESS, + referenceModuleInitData: [], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }; + }); + + it('reverts if called without going through lzGatedProxy', async () => { + await expect( + lensHub.commentWithSig(commentWithSigData) + ).to.be.revertedWith('CommentOrMirrorInvalid()'); + }); + + it('reverts if the caller does not have suffient balance', async () => { + await expect( + lzGatedProxy + .connect(userTwo) + .relayCommentWithSig( + userTwoAddress, + erc721.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + commentWithSigData + ) + ).to.be.revertedWith('InsufficientBalance'); + }); + + it('reverts if the contract call for balanceOf() fails', async () => { + await expect( + lzGatedProxy + .connect(userTwo) + .relayCommentWithSig( + userAddress, + lzEndpoint.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + commentWithSigData + ) + ).to.be.revertedWith('InsufficientBalance'); + }); + + it('[non-blocking] fails if the caller passed an invalid threshold', async () => { + await erc721.safeMint(userTwoAddress); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayCommentWithSig( + userTwoAddress, + erc721.address, + 0, + 0, // lzCustomGasAmount + commentWithSigData + ); + + const messageFailedReason = getNonBlockingError(await tx.wait()); + expect(messageFailedReason).to.equal('InvalidRemoteInput'); + }); + + + it('[non-blocking] fails if the caller passed an invalid token contract', async () => { + await erc20.mint(userTwoAddress, BALANCE_THRESHOLD); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayCommentWithSig( + userTwoAddress, + erc20.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + commentWithSigData + ); + + const messageFailedReason = getNonBlockingError(await tx.wait()); + expect(messageFailedReason).to.equal('InvalidRemoteInput'); + }); + + it('processes a valid comment', async () => { + await erc721.safeMint(userTwoAddress); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayCommentWithSig( + userTwoAddress, + erc721.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + commentWithSigData + ); + const logs = parseLogsNested(await tx.wait(), abi, LZGatedReferenceModuleABI.abi); + + const event = logs.find(({ name }) => name === 'MessageFailed'); + const commentEvent = logs.find(({ name }) => name === 'CommentCreated'); + + expect(event).to.equal(undefined); + expect(commentEvent).not.to.equal(undefined); + expect(commentEvent.args.pubIdPointed.toNumber()).to.equal(1); + expect(commentEvent.args.profileIdPointed.toNumber()).to.equal(1); + }); + }); + + describe('#processMirror (triggered from LZGatedProxy#relayMirrorWithSig)', () => { + let mirrorWithSigData; + let referenceModuleInitData; + + beforeEach(async() => { + const collectModuleInitData = ethers.utils.defaultAbiCoder.encode(['bool'], [true]); + referenceModuleInitData = ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint16'], + [erc721.address, BALANCE_THRESHOLD, REMOTE_CHAIN_ID] + ); + + await lensHub.post({ + profileId: FIRST_PROFILE_ID, + contentURI: MOCK_URI, + collectModule: freeCollectModule.address, + collectModuleInitData, + referenceModule: referenceModule.address, + referenceModuleInitData: referenceModuleInitData, + }); + + const nonce = (await lensHub.sigNonces(userTwoAddress)).toNumber(); + const { v, r, s } = await getMirrorWithSigParts( + userTwo as VoidSigner, + FIRST_PROFILE_ID + 1, + FIRST_PROFILE_ID, + '1', + [], + ZERO_ADDRESS, + [], + nonce, + MAX_UINT256 + ); + + // userTwo signs that they would like to mirror the user's first post + mirrorWithSigData = { + profileId: FIRST_PROFILE_ID + 1, + profileIdPointed: FIRST_PROFILE_ID, + pubIdPointed: '1', + referenceModuleData: [], + referenceModule: ZERO_ADDRESS, + referenceModuleInitData: [], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }; + }); + + it('reverts if called without going through lzGatedProxy', async () => { + await expect( + lensHub.mirrorWithSig(mirrorWithSigData) + ).to.be.revertedWith('CommentOrMirrorInvalid()'); + }); + + it('reverts if the caller does not have suffient balance', async () => { + await expect( + lzGatedProxy + .connect(userTwo) + .relayMirrorWithSig( + userTwoAddress, + erc721.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + mirrorWithSigData + ) + ).to.be.revertedWith('InsufficientBalance'); + }); + + it('reverts if the contract call for balanceOf() fails', async () => { + await expect( + lzGatedProxy + .connect(userTwo) + .relayMirrorWithSig( + userTwoAddress, + lzEndpoint.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + mirrorWithSigData + ) + ).to.be.revertedWith('InsufficientBalance'); + }); + + it('[non-blocking] fails if the caller passed an invalid threshold', async () => { + await erc721.safeMint(userTwoAddress); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayMirrorWithSig( + userTwoAddress, + erc721.address, + 0, + 0, // lzCustomGasAmount + mirrorWithSigData + ); + + const messageFailedReason = getNonBlockingError(await tx.wait()); + expect(messageFailedReason).to.equal('InvalidRemoteInput'); + }); + + + it('[non-blocking] fails if the caller passed an invalid token contract', async () => { + await erc20.mint(userTwoAddress, BALANCE_THRESHOLD); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayMirrorWithSig( + userTwoAddress, + erc20.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + mirrorWithSigData + ); + + const messageFailedReason = getNonBlockingError(await tx.wait()); + expect(messageFailedReason).to.equal('InvalidRemoteInput'); + }); + + it('processes a valid mirror', async () => { + await erc721.safeMint(userTwoAddress); + + const tx = await lzGatedProxy + .connect(userTwo) + .relayMirrorWithSig( + userTwoAddress, + erc721.address, + BALANCE_THRESHOLD, + 0, // lzCustomGasAmount + mirrorWithSigData + ); + const logs = parseLogsNested(await tx.wait(), abi, LZGatedReferenceModuleABI.abi); + + const event = logs.find(({ name }) => name === 'MessageFailed'); + const commentEvent = logs.find(({ name }) => name === 'MirrorCreated'); + + expect(event).to.equal(undefined); + expect(commentEvent).not.to.equal(undefined); + expect(commentEvent.args.pubIdPointed.toNumber()).to.equal(1); + expect(commentEvent.args.profileIdPointed.toNumber()).to.equal(1); + }); + }); +}); diff --git a/test/OmniSBT.spec.ts b/test/OmniSBT.spec.ts index a3894ee..3b3a53d 100644 --- a/test/OmniSBT.spec.ts +++ b/test/OmniSBT.spec.ts @@ -8,8 +8,6 @@ import { LZEndpointMock, LZEndpointMock__factory, } from '../typechain-types' -import { abi } from './../build/contracts/OmniSBT.sol/OmniSBT.json'; -import parseLogs from './utils/parseLogs'; import { ZERO_ADDRESS } from './lens/helpers/constants'; const CHAIN_ID = 123; diff --git a/test/lens/helpers/utils.ts b/test/lens/helpers/utils.ts index b54a6fd..73fe094 100644 --- a/test/lens/helpers/utils.ts +++ b/test/lens/helpers/utils.ts @@ -1,5 +1,5 @@ import '@nomiclabs/hardhat-ethers'; -import { BigNumberish, Bytes, logger, utils, BigNumber, Contract, Signer } from 'ethers'; +import { BigNumberish, Bytes, logger, utils, BigNumber, Contract, Signer, VoidSigner } from 'ethers'; import { eventsLib, helper, @@ -101,3 +101,226 @@ export async function takeSnapshot() { export async function revertToSnapshot() { await hre.ethers.provider.send('evm_revert', [snapshotId]); } + +export async function getFollowWithSigParts( + wallet: VoidSigner, + profileIds: string[] | number[], + datas: Bytes[] | string[], + nonce: number, + deadline: string +): Promise<{ v: number; r: string; s: string }> { + const msgParams = buildFollowWithSigParams(profileIds, datas, nonce, deadline); + return await getSig(wallet, msgParams); +} + +export async function getCommentWithSigParts( + wallet: VoidSigner, + profileId: BigNumberish, + contentURI: string, + profileIdPointed: BigNumberish, + pubIdPointed: string, + referenceModuleData: Bytes | string, + collectModule: string, + collectModuleInitData: Bytes | string, + referenceModule: string, + referenceModuleInitData: Bytes | string, + nonce: number, + deadline: string +): Promise<{ v: number; r: string; s: string }> { + const msgParams = buildCommentWithSigParams( + profileId, + contentURI, + profileIdPointed, + pubIdPointed, + referenceModuleData, + collectModule, + collectModuleInitData, + referenceModule, + referenceModuleInitData, + nonce, + deadline + ); + return await getSig(wallet, msgParams); +} + +export async function getMirrorWithSigParts( + wallet: VoidSigner, + profileId: BigNumberish, + profileIdPointed: BigNumberish, + pubIdPointed: string, + referenceModuleData: Bytes | string, + referenceModule: string, + referenceModuleInitData: Bytes | string, + nonce: number, + deadline: string +): Promise<{ v: number; r: string; s: string }> { + const msgParams = buildMirrorWithSigParams( + profileId, + profileIdPointed, + pubIdPointed, + referenceModuleData, + referenceModule, + referenceModuleInitData, + nonce, + deadline + ); + return await getSig(wallet, msgParams); +} + +export const buildCommentWithSigParams = ( + profileId: BigNumberish, + contentURI: string, + profileIdPointed: BigNumberish, + pubIdPointed: string, + referenceModuleData: Bytes | string, + collectModule: string, + collectModuleInitData: Bytes | string, + referenceModule: string, + referenceModuleInitData: Bytes | string, + nonce: number, + deadline: string +) => ({ + types: { + CommentWithSig: [ + { name: 'profileId', type: 'uint256' }, + { name: 'contentURI', type: 'string' }, + { name: 'profileIdPointed', type: 'uint256' }, + { name: 'pubIdPointed', type: 'uint256' }, + { name: 'referenceModuleData', type: 'bytes' }, + { name: 'collectModule', type: 'address' }, + { name: 'collectModuleInitData', type: 'bytes' }, + { name: 'referenceModule', type: 'address' }, + { name: 'referenceModuleInitData', type: 'bytes' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + domain: domain(), + value: { + profileId: profileId, + contentURI: contentURI, + profileIdPointed: profileIdPointed, + pubIdPointed: pubIdPointed, + referenceModuleData: referenceModuleData, + collectModule: collectModule, + collectModuleInitData: collectModuleInitData, + referenceModule: referenceModule, + referenceModuleInitData: referenceModuleInitData, + nonce: nonce, + deadline: deadline, + }, +}); + +export const buildMirrorWithSigParams = ( + profileId: BigNumberish, + profileIdPointed: BigNumberish, + pubIdPointed: string, + referenceModuleData: Bytes | string, + referenceModule: string, + referenceModuleInitData: Bytes | string, + nonce: number, + deadline: string +) => ({ + types: { + MirrorWithSig: [ + { name: 'profileId', type: 'uint256' }, + { name: 'profileIdPointed', type: 'uint256' }, + { name: 'pubIdPointed', type: 'uint256' }, + { name: 'referenceModuleData', type: 'bytes' }, + { name: 'referenceModule', type: 'address' }, + { name: 'referenceModuleInitData', type: 'bytes' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + domain: domain(), + value: { + profileId: profileId, + profileIdPointed: profileIdPointed, + pubIdPointed: pubIdPointed, + referenceModuleData: referenceModuleData, + referenceModule: referenceModule, + referenceModuleInitData: referenceModuleInitData, + nonce: nonce, + deadline: deadline, + }, +}); + +export const buildFollowWithSigParams = ( + profileIds: string[] | number[], + datas: Bytes[] | string[], + nonce: number, + deadline: string +) => ({ + types: { + FollowWithSig: [ + { name: 'profileIds', type: 'uint256[]' }, + { name: 'datas', type: 'bytes[]' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + domain: domain(), + value: { + profileIds: profileIds, + datas: datas, + nonce: nonce, + deadline: deadline, + }, +}); + +export async function getCollectWithSigParts( + wallet: VoidSigner, + profileId: BigNumberish, + pubId: string, + data: Bytes | string, + nonce: number, + deadline: string +): Promise<{ v: number; r: string; s: string }> { + const msgParams = buildCollectWithSigParams(profileId, pubId, data, nonce, deadline); + return await getSig(wallet, msgParams); +} + +export const buildCollectWithSigParams = ( + profileId: BigNumberish, + pubId: string, + data: Bytes | string, + nonce: number, + deadline: string +) => ({ + types: { + CollectWithSig: [ + { name: 'profileId', type: 'uint256' }, + { name: 'pubId', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + domain: domain(), + value: { + profileId: profileId, + pubId: pubId, + data: data, + nonce: nonce, + deadline: deadline, + }, +}); + +async function getSig(wallet: VoidSigner, msgParams: { + domain: any; + types: any; + value: any; +}): Promise<{ v: number; r: string; s: string }> { + const sig = await wallet._signTypedData(msgParams.domain, msgParams.types, msgParams.value); + return utils.splitSignature(sig); +} + +function domain(): { name: string; version: string; chainId: number; verifyingContract: string } { + return { + name: LENS_HUB_NFT_NAME, + version: '1', + chainId: getChainId(), + verifyingContract: lensHub.address, + }; +} diff --git a/test/utils/parseLogs.ts b/test/utils/parseLogs.ts index 2285048..a4584a3 100644 --- a/test/utils/parseLogs.ts +++ b/test/utils/parseLogs.ts @@ -1,6 +1,6 @@ import { ethers } from 'hardhat'; -const parseLogs = ({ logs }, abi) => { +export const parseLogs = ({ logs }, abi) => { const iface = new ethers.utils.Interface(abi); return (logs.map((log) => { try { return iface.parseLog(log); } @@ -8,4 +8,14 @@ const parseLogs = ({ logs }, abi) => { })).filter((l) => l); }; -export default parseLogs; +export const parseLogsNested = ({ logs }, abi, nestedAbi) => { + const iface = new ethers.utils.Interface(abi); + const ifaceNested = new ethers.utils.Interface(nestedAbi); + return (logs.map((log) => { + try { return iface.parseLog(log); } + catch { + try { return ifaceNested.parseLog(log); } + catch {} + } + })).filter((l) => l); +}; diff --git a/yarn.lock b/yarn.lock index 57ff6e8..007df8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7302,6 +7302,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +promise-limit@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/promise-limit/-/promise-limit-2.7.0.tgz#eb5737c33342a030eaeaecea9b3d3a93cb592b26" + integrity sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw== + promise-to-callback@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/promise-to-callback/-/promise-to-callback-1.0.0.tgz#5d2a749010bfb67d963598fcd3960746a68feef7"