[Testnet] Open Action | Cashtags

Earn trading fees from trades on your Lens profile

ℹ️ This technical guide will go over our open action from a protocol perspective, and then with a full typescript example to initialize and process a post with the MoneyClubsAction (DEPRECATED)

Smart Contract Overview (Base Sepolia)

MoneyClubs

Contract events can be queried via TheGraph

The MoneyClubsAction module enables trading for Lens handles - or cashtags - on a bonding curve. Anyone can enable trades for their cashtag by purchasing the initial supply for their Lens handle, which enables the bonding curve. This is similar to friendtech, but with less aggressive prices, and with six decimals of precision. As people buy/sell the $cashtag, the protocol fees get split between the protocol, creator, and client address (if any).

The action module sends transactions to the core contract MoneyClubs - which is callable from outside the open action. The open action simply enables trades on a Lens post, and users the Lens profile owner as the beneficiary address. By separating the logic into two contracts, we can also invite participants from outside the Lens ecosystem.

ℹ️ To query contract state and get current prices, you will call the MoneyClubs contract

Anyone can register their cashtag by calling the MoneyClubs contract, and approving the required $bonsai to pay for the initial supply required. This registration only needs to be done once, and can be done via the open action to announce the cashtag via a Lens post. Subsequent posts with the open action will simply enable trades on the post.

ℹ️ We use the terms club and cashtag interchangeably. The final product name is subject to change.

Now we'll review the main contract functions for MoneyClubs - without the open action

MoneyClubs Contract

Register club

Anyone can register their cashtag by calling the appropriate function on the MoneyClubs contract.

  • To link it to a Lens profile, you must call the contract from the EOA of the Lens profile owner

  • You must purchase the initial supply of the bonding curve (max 10)

  • You must approve the correct amount of $bonsai to purchase the initial supply

The solidity functions to get the initial cost and register the cashtag are

/**
 * @notice Calculates the initial buy price for club creators to buy their own chips
 * @param amount The amount of club chips to buy (up to INITIAL_CHIP_SUPPLY_CAP)
 */
function getInitialBuyPrice(uint256 amount) public view returns (uint256)

/**
 * @notice Registers a new club with the info and mints the initial supply for `msg.sender`. If the feature flag
 * `allowAnyRegistration` is false, creators can only register their own clubs. Requires the caller to pay for the
 * initial supply, to init the bonding curve.
 * @param creator The creator to register the club for
 * @param initialSupply The initial supply for the club chips
 * @param curve To set the steepness of the curve
*/
function registerClub(address creator, uint256 initialSupply, CURVE_TYPE curve) external

So to register the club, and pay for one share

// assuming clubs is a viem contract instance for MoneyClubs
// assuming bonsai is a viem contract instance for Bonsai
// assuming LENS_PROFILE_OWNER is an address

const curve = 0; // SOFT | NORMAL | STEEP
const amount = formatUnits(1, 6); // shares have 6 decimals of precision
const price = await clubs.getInitialBuyPrice(amount);

// approve the bonsai
let tx = await bonsai.approve(clubs.address, price);
await tx.wait();

// register and purchase the initial supply
tx = await clubs.registerClub(LENS_PROFILE_OWNER, amount, curve);
await tx.wait();

Get information for registered clubs

The index for clubs is the creator address, and you can get information via the MoneyClubs contract or the subgraph

enum CURVE_TYPE {
    SOFT,
    NORMAL,
    STEEP
}

struct ClubData {
    uint256 supply;
    uint256 createdAt;
    uint256 liquidity;
    CURVE_TYPE curve;
}

mapping (address creator => ClubData data) public registeredClubs;

Or query the subgraph to get information like the supply and marketCap . currentPrice might not be up-to-date, so make sure to query the contract for executing trades. When looking at the trades array, the id refers to the wallet address that

{
  club(id: "0x28ff8e457fef9870b9d1529fe68fbb95c3181f64") {
    id
    initialSupply
    createdAt
    supply
    currentPrice
    marketCap
    trades {
      trader { id }
      amount
      price
      txPrice
      txHash
    }
  }
}

Get a balance

To get the balance of shares in a club for a given address, you can call the balances view function on the MoneyClubs contract. The returned value will have 6 decimals of precision.

/**
 * @notice Returns the balance of shares the `account` has in the `creator` club
 */
function balances(address creator, address account) external view returns (uint256 amount)

Get current prices

To get the current price of the club, and include the protocol fees in the quote - you can call the MoneyClubs contract. Remember that amounts have 6 decimals of precision (ex: to buy one whole share, you will use amount = 1000000)

/**
 * @notice Calculates the buy price for `amount` of `creator` club chips after fees
 * @param creator The club creator
 * @param amount The amount of club chips to buy
 */
function getBuyPriceAfterFees(address creator, uint256 amount) external view returns (uint256)

To actually buy, the caller must approve the amount on the Bonsai token contract, with the MoneyClubs contract address as the operator.

To get the general cost of the next whole share, without fees, you can call the getBuyPrice function with 1000000 as the input amount.

/**
 * @notice Calculates the buy price for a given `creator` club
 * @param creator The club creator
 * @param amount The amount of club chips to buy
 */
function getBuyPrice(address creator, uint256 amount) external view returns (uint256)

To get the amount of $BONSAI the caller will get back for selling their shares, you can call the getSellPriceAfterFees function.

/**
 * @notice Calculates the sell price for `amount` of `creator` club chips after fees
 * @param creator The club creator
 * @param amount The amount of club chips to sell
 */
function getSellPriceAfterFees(address creator, uint256 amount) external view returns (uint256)

Buy

Anyone can buy into a club if they know the creator address. The caller must first approve the cost in $bonsai for the amount of shares they want to buy via getBuyPriceAfterFees (see above)

/**
 * @notice Allows anyone to buy an `amount` of chips from a registered club by the `creator`. Must have approved the
 * price returned from `#getBuyPriceAfterFees` on Bonsai.
 * NOTE: Fees are split between protocol/creator/client, and stored in the contract.
 * @param creator The club creator to buy chips for
 * @param amount The amount of club chips to buy
 * @param clientAddress The client address to receive fee split
 * @param recipient The recipient of the chips
 */
function buyChips(address creator, uint256 amount, address clientAddress, address recipient)
external

Sell

Anyone can sell their shares of a club by calling the sellChips function on the MoneyClubs contract. After calling the sellChips function, they will receive the $BONSAI (minus fees) and have their balance reflected

/**
 * @notice Allows anyone to sell an `amount` of chips from a registered club by the `creator`.
 * NOTE: Fees are split between protocol/creator/client, and stored in the contract.
 * @param creator The club creator to sell chips for
 * @param amount The amount of club chips to sell
 * @param clientAddress The client address to receive fee split
 */
function sellChips(address creator, uint256 amount, address clientAddress) external

Withdraw Fees

The treasury address or any client address can withdraw fees at any time. There is also the option to choose to swap a % of the amount from $BONSAI to WETH.

First, read the total amount of fees earned, and then call withdrawFeesEarned with that amount (or less) and the percentage swapNativePct (in bps) to swap into WETH.

function feesEarned(address account) external view returns (uint256 amount);
/**
 * @notice Allows anyone (treasury, creator, client) to withdraw fees earned
 * @param amount The amount of fees to withdraw
 * @param swapNativePct The percentage (in bps) of `amount` to swap into WETH
 * @param swapFee The fee for the univ3 pool to swap through [500 | 3000 | 10000]
 */
function withdrawFeesEarned(uint256 amount, uint16 swapNativePct, uint24 swapFee) external

MoneyClubsAction Module

🚧 This is only valid for the Polygon deployment - LIKELY TO BE DEPRECATED

Init & register club

A Lens profile can create a post and register their club at the same time. Although they can only register their club once, they can always create a post that points to their club, to allow trading.

Same process as above; to register the club, and pay for one share

// 1. get the cost for the initial supply of one share
const initialSupply = parseUnits('1', 6);  // 6 decimal points
const initCost = await clubs.getInitialBuyPrice(initialSupply);

// 2. approve the $bonsai to `MoneyClubs` contract
let tx = await token.approve(clubs.address, initCost);
await tx.wait();

// 3. encode the data
const actionModuleData = await encodeModuleInitData(actionModule.address as `0x${string}`, {
  initialSupply,
  club: zeroAddress // or the creator address of an already registered club
});

// 4. send the create post tx (via api or contract)

Init & promote club

Anyone can promote a club an allow trading on their post by providing the club creator address in the init data. Looking at the example above, when creating the init data, we use 0 as the initialSupply and can skip the $BONSAI approve step, and set the club address.

// 3. encode the data
const actionModuleData = await encodeModuleInitData(actionModule.address as `0x${string}`, {
  '0', // since we are not registering
  club: "0x..." // the creator address of an already registered club
});

Buy

To buy via the open action, the club creator must first create a Lens post with the MoneyClubsAction module. The act tx would simply encode the correct payload including the amount of shares to purchase (accounting for 6 decimals of precision) and the optional clientAddress to split the protocol fee with. The typescript would look something like this:

// 1. get the cost (including fees)
const purchaseAmount = parseUnits('1', 6);  // 6 decimal points
const buyCost = await clubs.getBuyPriceAfterFees(CREATOR_ADDRESS, purchaseAmount);

// 2. approve the $bonsai to `MoneyClubs` contract
let tx = await token.approve(clubs.address, buyCost);
await tx.wait();

// 3. encode the data
const actType = 0; // BUY | SELL
const clientAddress = ""; // to split protocol fee with a lens client
const actionModuleData = await encodeModuleActData(actionModule.address as `0x${string}`, {
  actType,
  amount: purchaseAmount,
  clientAddress
});

// 4. send the act tx (via api or contract)

Sell

Like for buying, the club creator must first create a Lens post with the MoneyClubsAction module in order for anyone to be able to sell via the open action. The act tx would simply encode the correct payload including the amount of shares to sell (accounting for 6 decimals of precision) and the optional clientAddress to split the protocol fee with. The typescript would look something like this:

// 1. get the sell price (minus fees)
const sellAmount = parseUnits('1', 6);  // 6 decimal points
const sellPrice = await clubs.getSellPriceAfterFees(CREATOR_ADDRESS, sellAmount);
console.log(`receiving ${sellPrice} $BONSAI for selling 1 whole share`); 

// 2. encode the data
const actType = 1; // BUY | SELL
const clientAddress = ""; // to split protocol fee with a lens client
const actionModuleData = await encodeModuleActData(actionModule.address as `0x${string}`, {
  actType,
  amount: sellAmount,
  clientAddress
});

// 3. send the act tx (via api or contract)

Last updated