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.
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.
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) */functiongetInitialBuyPrice(uint256 amount) publicviewreturns (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*/functionregisterClub(addresscreator,uint256initialSupply,CURVE_TYPEcurve) 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 addressconstcurve=0; // SOFT | NORMAL | STEEPconstamount=formatUnits(1,6); // shares have 6 decimals of precisionconstprice=awaitclubs.getInitialBuyPrice(amount);// approve the bonsailet tx =awaitbonsai.approve(clubs.address, price);awaittx.wait();// register and purchase the initial supplytx =awaitclubs.registerClub(LENS_PROFILE_OWNER, amount, curve);awaittx.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
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
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 */functionbalances(address creator,address account) externalviewreturns (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 */functiongetBuyPriceAfterFees(address creator,uint256 amount) externalviewreturns (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 */functiongetBuyPrice(address creator,uint256 amount) externalviewreturns (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 */functiongetSellPriceAfterFees(address creator,uint256 amount) externalviewreturns (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 */functionbuyChips(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 */functionsellChips(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.
/** * @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] */functionwithdrawFeesEarned(uint256 amount,uint16 swapNativePct,uint24 swapFee) external
MoneyClubsAction Module
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 shareconstinitialSupply=parseUnits('1',6); // 6 decimal pointsconstinitCost=awaitclubs.getInitialBuyPrice(initialSupply);// 2. approve the $bonsai to `MoneyClubs` contractlet tx =awaittoken.approve(clubs.address, initCost);awaittx.wait();// 3. encode the dataconstactionModuleData=awaitencodeModuleInitData(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 dataconstactionModuleData=awaitencodeModuleInitData(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)constpurchaseAmount=parseUnits('1',6); // 6 decimal pointsconstbuyCost=awaitclubs.getBuyPriceAfterFees(CREATOR_ADDRESS, purchaseAmount);// 2. approve the $bonsai to `MoneyClubs` contractlet tx =awaittoken.approve(clubs.address, buyCost);awaittx.wait();// 3. encode the dataconstactType=0; // BUY | SELLconstclientAddress=""; // to split protocol fee with a lens clientconstactionModuleData=awaitencodeModuleActData(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)constsellAmount=parseUnits('1',6); // 6 decimal pointsconstsellPrice=awaitclubs.getSellPriceAfterFees(CREATOR_ADDRESS, sellAmount);console.log(`receiving ${sellPrice} $BONSAI for selling 1 whole share`); // 2. encode the dataconstactType=1; // BUY | SELLconstclientAddress=""; // to split protocol fee with a lens clientconstactionModuleData=awaitencodeModuleActData(actionModule.address as`0x${string}`, { actType, amount: sellAmount, clientAddress});// 3. send the act tx (via api or contract)
To query contract state and get current prices, you will call the MoneyClubs contract
We use the terms club and cashtag interchangeably. The final product name is subject to change.
This is only valid for the Polygon deployment - LIKELY TO BE DEPRECATED