Skip to content

Commit

Permalink
Fist version
Browse files Browse the repository at this point in the history
  • Loading branch information
mvanhalen committed Jan 8, 2024
0 parents commit 77ab6c4
Show file tree
Hide file tree
Showing 298 changed files with 61,199 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENCE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 BuidlGuidl

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
192 changes: 192 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# 🏗 TipActionModule

The `TipActionModule` is a simple module that allows users to tip the author of a publication. It's based on the "Creating an Open Action" tutorial from the [Lens Docs](https://docs.lens.xyz/docs/creating-a-publication-action). And is based on [Scaffold-Lens](https://github.com/iPaulPro/scaffold-lens)

Additions to the tutorial include:
- ✅ Adds compliance with the Open Action [Module Metadata Standard](https://docs.lens.xyz/docs/module-metadata-standard)
- ✅ Checks token allowance before attempting to send a tip
- ✅ Uses `SafeERC20` to transfer tokens
- ✅ Uses `ReentrancyGuard` to prevent reentrancy attacks


## Contents

- [Requirements](#requirements)
- [Debugging](#debugging)
- [Using the TipActionModule Contract](#using-the-tipactionmodule-contract)
- [About Scaffold-ETH 2](#about-scaffold-eth-2)

## Requirements

Before you begin, you need to install the following tools:

- [Node (v18 LTS)](https://nodejs.org/en/download/)
- [Git](https://git-scm.com/downloads)



## Debugging

You can debug your smart contracts using the Contract Debugger. If you haven't already, from the root directory, start your NextJS app:
```shell
yarn start
```

Then navigate to http://localhost:3000/debug to open the debugger. You can now call functions on your smart contracts and debug them in the browser.

### Debugging the `TipActionModule`


**Tip:** Use https://abi.hashex.org/ to encode the calldata for the `initializePublicationAction` and `processPublicationAction` functions.



## Using the TipActionModule Contract

To use the live `TipActionModule` you can use the address and metadata below:

| Network | Chain ID | Deployed Contract | Metadata |
|---------|----------|---------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| Mumbai | 80001 | [0xbb1B9C55F943Bd2edCa0cDfa2F4Bb41Ea2c49a44](https://mumbai.polygonscan.com/address/0xbb1B9C55F943Bd2edCa0cDfa2F4Bb41Ea2c49a44) | [link](https://devnet.irys.xyz/DmTRxYr0BR02aWUZ2R8UB12RO31r4qmqScp9fuuNIRY) |

The `TipActionModule` contract can be used as an Open Action Module on Lens Protocol.

The initialize calldata ABI is

```json
[
{
"type": "address",
"name": "tipReceiver"
}
]
```

And the process calldata ABI is

```json
[
{
"type": "address",
"name": "currency"
},
{
"type": "uint256",
"name": "tipAmount"
}
]
```

Where `currency` is the address of the ERC20 token to use for tips, and `tipAmount` is the amount of tokens to send in wei.

**NOTE:** The token used for tipping must be registered with the Lens Module Registry.

### Using the TipActionModule with the Lens SDK

You can initialize the action and set the tip receiver using the Lens SDK Client.

```typescript
import { type LensClient, type OnchainPostRequest, encodeData } from '@lens-protocol/client';

const openActionContract = "0xce962e5ade34202489e04bb8ed258f8d079eee3e";

const postRequest: OnchainPostRequest = { contentURI };

// The initialization calldata accepts a single address parameter, the tip receiver
const calldata = encodeData(
[{ name: "tipReceiver", type: "address" }],
[tipReceiver],
);

postRequest.openActionModules.push({
unknownOpenAction: {
address: openActionContract,
data: calldata,
},
});

await lensClient.publication.postOnchain(postRequest);
```

To support executing a tip action, you can create an `act` transaction as usual, supplying the currency and amount to tip as the process call data.

```typescript
const tipActionContract = "0xce962e5ade34202489e04bb8ed258f8d079eee3e";

// get the module settings and metadata
const settings = post.openActionModules.find(
(module) => module.contract.address.toLowerCase() === tipActionContract.toLowerCase(),
);
const metadataRes = await lensClient.modules.fetchMetadata({
implementation: settings.contract.address,
});

// encode calldata
const processCalldata: ModuleParam[] = JSON.parse(metadataRes.metadata.processCalldataABI);
const calldata = encodeData(
processCalldata,
[currency, amount.toString()],
);

// create the act transaction request
const request: ActOnOpenActionRequest = {
actOn: {
unknownOpenAction: {
address: tipActionContract,
data: calldata,
},
},
for: post.id,
};

const tip = await lensClient.publication.actions.createActOnTypedData(request);
// sign and broadcast transaction
```

The tip receiver address can be obtained from the module settings:
```typescript
if (settings.initializeCalldata) {
const initData = decodeData(
JSON.parse(metadataRes.metadata.initializeCalldataABI) as ModuleParam[],
settings.initializeCalldata,
);
// This is the tip receiver address (registered in the initialize calldata)
const tipReceiver = initData.tipReceiver;
}
```

### Important Notes

1. Clients implementing the tip action should ensure the user has approved the tip amount for the tip currency before attempting to execute the tip action.

You can check the allowance using the ERC-20 `allowance` function directly, or use the Lens SDK Client:

```typescript
const needsApproval = async (
lensClient: LensClient,
currency: string,
tipAmount: BigNumber,
actionModule: string,
): Promise<boolean> => {
const req: ApprovedModuleAllowanceAmountRequest = {
currencies: [currency],
unknownOpenActionModules: [actionModule],
};

const res = await lensClient.modules.approvedAllowanceAmount(req);
if (res.isFailure()) return true;

const allowances = res.unwrap();
if (!allowances.length) return true;

const valueInWei = ethers.utils.parseEther(allowances[0].allowance.value);
return tipAmount.gt(valueInWei);
};
```
---

## About Scaffold-Lens

Scaffold-Lens is an open-source toolkit made by Paul Burke for building Lens Smart Posts and Open Actions dapps.

Learn more about Scaffold-Lens and read the docs [here](https://github.com/iPaulPro/scaffold-lens).
154 changes: 154 additions & 0 deletions contracts/TipActionModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Types} from "./libraries/Types.sol";
import {IPublicationActionModule} from "./interfaces/IPublicationActionModule.sol";
import {HubRestricted} from "./base/HubRestricted.sol";
import {IModuleRegistry} from "./interfaces/IModuleRegistry.sol";
import {LensModuleMetadata} from "./base/LensModuleMetadata.sol";
import {LensModuleRegistrant} from "./base/LensModuleRegistrant.sol";

/**
* @title TipActionModule
* @dev Open Action Module for tipping Lens publications.
*/
contract TipActionModule is
IPublicationActionModule,
Ownable,
HubRestricted,
LensModuleMetadata,
LensModuleRegistrant,
ReentrancyGuard
{
using SafeERC20 for IERC20;

error CurrencyNotWhitelisted();
error TipAmountCannotBeZero();
error TipReceiverNotProvided();
error TipAmountNotApproved();

event TipReceiverRegistered(
uint256 indexed profileId,
uint256 indexed pubId,
address indexed tipReceiver
);

event TipCreated(
address indexed transactionExecutor,
address indexed tipReceiver,
address indexed currency,
uint256 tipAmount
);

/**
* @dev Mapping of tip receivers for publications.
*/
mapping(uint256 profileId => mapping(uint256 pubId => address tipReceiver))
internal _tipReceivers;

/**
* @dev Initializes the TipActionModule contract.
* @param hub Address of the LensHub contract.
* @param moduleRegistry Address of the ModuleRegistry contract.
*/
constructor(
address hub,
address moduleRegistry
)
Ownable()
HubRestricted(hub)
LensModuleMetadata()
LensModuleRegistrant(moduleRegistry)
{}

/**
* @dev Returns the tip receiver for a publication.
* @param profileId ID of the profile.
* @param pubId ID of the publication.
* @return Address of the tip receiver.
*/
function getTipReceiver(
uint256 profileId,
uint256 pubId
) public view returns (address) {
return _tipReceivers[profileId][pubId];
}

function supportsInterface(
bytes4 interfaceID
) public pure virtual override returns (bool) {
return
interfaceID == type(IPublicationActionModule).interfaceId ||
super.supportsInterface(interfaceID);
}

function initializePublicationAction(
uint256 profileId,
uint256 pubId,
address /* transactionExecutor */,
bytes calldata data
) external override onlyHub returns (bytes memory) {
address tipReceiver = abi.decode(data, (address));

if (tipReceiver == address(0)) {
revert TipReceiverNotProvided();
}

_tipReceivers[profileId][pubId] = tipReceiver;

emit TipReceiverRegistered(profileId, pubId, tipReceiver);

return data;
}

function processPublicationAction(
Types.ProcessActionParams calldata params
) external override onlyHub nonReentrant returns (bytes memory) {
(address currency, uint256 tipAmount) = abi.decode(
params.actionModuleData,
(address, uint256)
);

if (!MODULE_REGISTRY.isErc20CurrencyRegistered(currency)) {
revert CurrencyNotWhitelisted();
}

if (tipAmount == 0) {
revert TipAmountCannotBeZero();
}

address tipReceiver = _tipReceivers[params.publicationActedProfileId][
params.publicationActedId
];

IERC20 token = IERC20(currency);

uint256 allowance = token.allowance(
params.transactionExecutor,
address(this)
);

if (allowance < tipAmount) {
revert TipAmountNotApproved();
}

emit TipCreated(
params.transactionExecutor,
tipReceiver,
currency,
tipAmount
);

token.safeTransferFrom(
params.transactionExecutor,
tipReceiver,
tipAmount
);

return abi.encode(tipReceiver, currency, tipAmount);
}
}
27 changes: 27 additions & 0 deletions contracts/base/HubRestricted.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import {Errors} from "../libraries/Errors.sol";

/**
* @title HubRestricted
* @author Lens Protocol
*
* @notice This abstract contract adds a public `HUB` immutable field, as well as an `onlyHub` modifier,
* to inherit from contracts that have functions restricted to be only called by the Lens hub.
*/
abstract contract HubRestricted {
address public immutable HUB;

modifier onlyHub() {
if (msg.sender != HUB) {
revert Errors.NotHub();
}
_;
}

constructor(address hub) {
HUB = hub;
}
}
Loading

0 comments on commit 77ab6c4

Please sign in to comment.