Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature / lz gated modules #26

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
ETHERSCAN_API_KEY=
ROPSTEN_URL=
PRIVATE_KEY=
MUMBAI_RPC_URL=
POLYGON_RPC_URL=
GOERLI_RPC_URL=
BLOCK_EXPLORER_KEY=

# FOUNDRY:
export MNEMONIC=""
Expand All @@ -9,6 +13,7 @@ export MNEMONIC=""
export PRIVATE_KEY=""
export POLYGON_RPC_URL=
export MUMBAI_RPC_URL=
export GOERLI_RPC_URL=
export BLOCK_EXPLORER_KEY=
export MAINNET_EXPLORER_API=https://api.polygonscan.com/api/
export TESTNET_EXPLORER_API=https://api-testnet.polygonscan.com/api/
Expand Down
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,51 @@ This repository contains both - Hardhat and Foundry tests. Foundry will be used
3. Follow the on-screen instructions to verify if everything is correct and confirm deployment & contract verification.
4. If only the verification is needed of an existing deployed contract - use the `--verify-only` flag followed by ABI-Encoded constructor args.

## Deployement addresses in `addresses.json`
### Deployment of LZGated* Modules
All the hardhat tasks related to the deployment of `LZGatedFollowModule`, `LZGatedReferenceModule`, and `LZGatedCollectModule` can be found under [**tasks/lz-gated/**](./tasks/lz-gated)

First, we deploy the modules on the source chain (ex: `mumbai`) and we deploy our `LZGatedProxy` contract to all the remote chains we want to support (ex: `goerli`). Finally, we set the trusted remotes for each module. All the lz config can be found under [**tasks/lz-gated/config.ts**](./tasks/lz-gated/config.ts).

Contract addresses for new deployments will be written to `addresses.json`. For the contracts deployed to remote chains, a special property `lz` contains an object with those contract addresses.

1. deploy our modules on the same chain as the lens protocol, using the mock sandbox governance contract to whitelist.
```
npx hardhat deploy-modules --hub 0x7582177F9E536aB0b6c721e11f383C326F2Ad1D5 --mock-sandbox-governance 0x1677d9cc4861f1c85ac7009d5f06f49c928ca2ad --network mumbai
```
2. deploy our `LZGatedProxy` contract on all the remote chains we want to support
```
npx hardhat deploy-proxy --network goerli --sandbox true
```

3. set our trusted remotes
```
npx hardhat set-trusted-remotes --network mumbai --sandbox true
```

#### Testing on testnet
To test any of the three modules, run one of the `set-*-module` tasks to initialize it before running the associated `relay-*-with-sig` task. For example, let's test the follow module.

1. **You must update the variable `SANDBOX_USER_PROFILE_ID` in `tasks/lz-gated/config.ts`, and change the `TOKEN_*` variables to your desired configuration**. Anyone that wishes to follow `SANDBOX_USER_PROFILE_ID` must have a balance greater than or equal to `TOKEN_THRESHOLD` of an ERC20/ERC721 `TOKEN_CONTRACT` on the chain with lz chain id of `TOKEN_CHAIN_ID`
```
npx hardhat set-follow-module --network mumbai --hub 0x7582177F9E536aB0b6c721e11f383C326F2Ad1D5 --sandbox true
```

2. Anyone wishing to follow must sign the message for `LensHub#followWithSig` and submit it to our `LZGatedProxy` contract deployed on the chain with lz chain id `TOKEN_CHAIN_ID`
```
npx hardhat relay-follow-with-sig --network goerli --hub 0x7582177F9E536aB0b6c721e11f383C326F2Ad1D5 --sandbox true
```

#### Things to note
- Anyone can submit the tx to `LZGatedProxy` on behalf of the signer, but the tx `value` must be sufficient to pay the lz fee
- We accept a `lzCustomGasAmount` argument in each of the `#relay*` functions in `LZGatedProxy` for flexibility per transaction; it can be set to `0` which will default to the lz estimated gas. But if provided, tx `value` must cover this gas amount, and if _either_ the fee is not enough _or_ the gas is not enough for the execution, the tx at the destination chain will revert; [see more about lz adapter params](https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters). Each task has an `ESTIMATED_GAS_REMOTE` to showcase the estimated gas per action
- Our modules implement the `NonblockingLzApp` strategy in that we catch any reverts. We do this because layerzero would stop relaying messages to our receiver contract on the destination chain if any relayed messages are to revert; [see more in the lz docs](https://layerzero.gitbook.io/docs/evm-guides/advanced/nonblockinglzapp)
- We have the testnet setup for relayed lz messages betweed goerli/mumbai, but more chains are supported; [see the list of lz supported chains](https://layerzero.gitbook.io/docs/technical-reference/mainnet/supported-chain-ids)
- There is an "unused" variable in the `LzApp` contract called `zroPaymentAddress` - it set to the zero address. Assuming there will be a ZRO token, the contract deployer can set the variable to some paymaster address so that all relayed messages are sponsored and paid in ZRO tokens; [see more in the lz docs](https://layerzero.gitbook.io/docs/evm-guides/master/how-to-send-a-message)

## Deployment addresses in `addresses.json`

The `addresses.json` file in root contains all existing deployed contracts on all of target environments (mainnet/testnet/sandbox) on corresponding chains.
After a succesful module deployment the new address will be added to `addresses.json`, overwriting the existing one (the script will ask for confirmation if you want to redeploy an already existing deployment).
After a successful module deployment the new address will be added to `addresses.json`, overwriting the existing one (the script will ask for confirmation if you want to redeploy an already existing deployment).

## Coverage

Expand All @@ -54,10 +95,13 @@ After a succesful module deployment the new address will be added to `addresses.
- [**Multirecipient Fee Collect Module**](./contracts/collect/MultirecipientFeeCollectModule.sol): Fee Collect module that allows multiple recipients (up to 5) with different proportions of fees payout.
- [**Simple Fee Collect Module**](./contracts/collect/SimpleFeeCollectModule.sol): A simple fee collect module implementation, as an example of using base fee collect module abstract contract.
- [**Updatable Ownable Fee Collect Module**](./contracts/collect/UpdatableOwnableFeeCollectModule.sol): A fee collect module that, for each publication that uses it, mints an ERC-721 ownership-NFT to its author. Whoever owns the ownership-NFT has the rights to update the parameters required to do a successful collect operation over its underlying publication.
- [**LayerZero Gated Collect Module**](./contracts/collect/LZGatedCollectModule.sol): A Lens Collect Module that allows publication creators to gate who can collect their post with ERC20 or ERC721 balances held on other chains. To execute a collect on a post that has this module set, the collector must generate the signature for `LensHub#collectWithSig` and call `#relayCollectWithSig` on the `LZGatedProxy` contract deployed on the chain where the token balance check is done.

## Follow modules
- [**LayerZero Gated Follow Module**](./contracts/follow/LZGatedFollowModule.sol): A Lens Follow Module that allows profile holders to gate their following with ERC20 or ERC721 balances held on other chains. To execute a follow on a profile that has this module set, the follower must generate the signature for `LensHub#followWithSig` and call `#relayFollowWithSig` on the `LZGatedProxy` contract deployed on the chain where the token balance check is done.

## Reference modules

- [**Degrees Of Separation Reference Module**](./contracts/reference/DegreesOfSeparationReferenceModule.sol): This reference module allows to set a degree of separation `n`, and then allows to comment/mirror only to profiles that are at most at `n` degrees of separation from the author of the root publication.
- [**Token Gated Reference Module**](./contracts/reference/TokenGatedReferenceModule.sol): A reference module that validates that the user who tries to reference has a required minimum balance of ERC20/ERC721 token.
- [**LayerZero Gated Reference Module**](./contracts/reference/LZGatedReferenceModule.sol): 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. To execute a comment or mirror on a post that has this module set, the commentor/mirrorer must generate the signature for `LensHub#commentWithSig`/`LensHub#mirrorWithSig` and call `#relayCommentWithSig`/`#relayMirrorWithSig` on the `LZGatedProxy` contract deployed on the chain where the token balance check is done.
10 changes: 9 additions & 1 deletion addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@
"StepwiseCollectModule": "0x6928d6127dfa0da401737e6ff421fcf62d5617a3",
"ERC4626FeeCollectModule": "0x31126c602cf88193825a99dcd1d17bf1124b1b4f",
"AaveFeeCollectModule": "0x666e06215747879ee68b3e5a317dcd8411de1897",
"TokenGatedReferenceModule": "0x86d35562ceb9f10d7c2c23c098dfeacb02f53853"
"TokenGatedReferenceModule": "0x86d35562ceb9f10d7c2c23c098dfeacb02f53853",
"LZGatedFollowModule": "0x55B31858522479Ed90A4D92159B2F66c4C2EF847",
"LZGatedReferenceModule": "0x5c202A503BEbD30D6F95fc1CE2656269a999B83b",
"LZGatedCollectModule": "0x39eedD6f83ecA281FF3b2D817026B575e2980c79",
"lz": {
"goerli": {
"LZGatedProxy": "0x57F3C223FF7D508eb76725081560e4Aa24669b15"
}
}
}
}
131 changes: 131 additions & 0 deletions contracts/collect/LZGatedCollectModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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 {NonblockingLzApp} from "../lz/NonblockingLzApp.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, NonblockingLzApp {
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) NonblockingLzApp(_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
*/
function _nonblockingLzReceive(
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) {
revert InvalidRemoteInput();
}

validatedCollectors[collectSig.profileId][collectSig.pubId][collectSig.collector] = true;

// use the signature to execute the collect
ILensHub(HUB).collectWithSig(collectSig);

delete validatedCollectors[collectSig.profileId][collectSig.pubId][collectSig.collector];
}
}
136 changes: 136 additions & 0 deletions contracts/follow/LZGatedFollowModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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 {NonblockingLzApp} from "../lz/NonblockingLzApp.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, NonblockingLzApp {
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) NonblockingLzApp(_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
*/
function _nonblockingLzReceive(
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) {
revert InvalidRemoteInput();
}

// allow the follow in the callback to #processFollow
validatedFollowers[profileId][followSig.follower] = true;

// use the signature to execute the follow
ILensHub(HUB).followWithSig(followSig);

delete validatedFollowers[profileId][followSig.follower];
}
}
Loading