Cover image
MAR 13, 2024

On-chain Royalty Fees in Access Protocol V2

by Vlad, Access Protocol

Deep dive into the royalty fees that have been implemented in the Access Protocol V2 on-chain program and are soon coming to the user-facing frontend applications.
Vlad is a Fullstack Software Engineer at Access Protocol. You can reach out to him on X (@mmatdev) or via e-mail at vladislav@accessprotocol.co.
One important new feature that is coming in Access Protocol V2 are Royalties and Referral links. This will enable our users to get additional rewards by inviting their friends and followers.

The goal

Our main goal was to create an on-chain mechanism to reward the users that bring new people to the Access Protocol platform. The requirements were as follows:
  • The protocol inflation stays the same, the royalty tokens are not newly minted.
  • The royalty fees should be universal. They should be paid by the invited users as well as pool owners.
  • There should be a way to configure the royalty percentage as well as the duration how long the royalties are getting paid.
  • There should be a simple mechanism to help users that are scammed into the royalty payment.
  • Accepting the invitation should be as simple as just clicking a link.

The solution

After considering all the mentioned points we've decided to go with the following solution:
  • There is a Royalty Account PDA created on-chain for each user that is supposed to pay royalties. This account includes all the variables needed for the fee calculation.
  • The royalties will be calculated and paid out any time the invited user claims their rewards. The royalty percentage will be deducted from their rewards and sent to the wallet the invited them instead. These can be user rewards from locked or forever locked token or even pool.
  • There is a function to close the royalty account by the user that created it. This is only a on-chain safety feature to be able to mitigate issues for the users who accept an invitation by accident.

On-chain implementation

To achieve all the previously outlined goals we started by creating a new on-chain PDA with the following structure:
pub struct RoyaltyAccount { /// Tag to easily distinguish PDA types pub tag: Tag, /// The address that paid for the creation of this PDA - to be able to return funds on closing pub rent_payer: Pubkey, /// The address that has to pay royalties from all their claims pub royalty_payer: Pubkey, /// The address that collects the royalties pub recipient_ata: Pubkey, /// The date after which the royalties are not paid anymore pub expiration_date: u64, /// The royalty basis points (i.e 1% = 100) going to the recommender pub royalty_basis_points: u16, }
As you can see, this account includes all the information that we need for the calculation of the royalties as well as the expiration date.
The royalty account creation is as simple as you would expect. We just sanitize the accounts and then create a new PDA with the key derived from the Royalty payer public key. The instruction takes the following parameters:
  • Royalty basis points
  • Expiration date
  • The ATA that should be getting the ACS rewards
The full code can be found here.
On the other side there is an instruction for closing the Royalty Account PDA. This clears the account and transfers all the paid SOL fees to the original fee payer. Full code here.
The more interesting part is ensuring that the fees are actually getting paid on claim. We have added these two account to every claim instruction to pass information about the possible royalty requirements:
/// The owner's royalty split account to check if royalties need to be paid pub owner_royalty_account: &'a T, /// The royalty ATA account #[cons(writable)] pub royalty_ata: Option<&'a T>,
As you can see, the second account is optional - we only need it for the royalty payout in case the owner_royalty_account exists.
To check if the account exists and is not expired we have implemented the following function used in all claim instructions:
/// This function checks if there is an existing royalty account. /// Checks the relationship between the appropriate royalty account and the royalty ATA /// Returns the royalty account data if it exists. Otherwise returns None. pub fn retrieve_royalty_account( royalty_account: &AccountInfo, royalty_ata: Option<&AccountInfo>, ) -> Result<Option<RoyaltyAccount>, ProgramError> { if royalty_account.data_is_empty() { return Ok(None); } let royalty_account_data = RoyaltyAccount::from_account_info(royalty_account)?; if royalty_account_data.expiration_date < Clock::get()?.unix_timestamp as u64 { return Ok(None); // Royalty account has expired - no royalty split is applicable } if royalty_ata.is_none() { return Err(AccessError::RoyaltyAtaNotProvided.into()); } check_account_owner( royalty_ata.unwrap(), &spl_token::ID, AccessError::WrongOwner, )?; check_account_key( royalty_ata.unwrap(), &royalty_account_data.recipient_ata, AccessError::RoyaltyAtaNotDeterministic, )?; Ok(Some(royalty_account_data)) }
This function verifies that the supplied accounts are correct and if so, it returns either the Royalty Account or None, if it doesn't exist.
Then we only need to calculate the reward split like this:
// Calculate the rewards (checks if the pool is cranked as well) let mut reward = [...] // split the rewards if there is a royalty account let mut royalty_amount = 0; if let Some(royalty_account) = royalty_account_data { royalty_amount = royalty_account.calculate_royalty_amount(reward)?; reward = reward.checked_sub(royalty_amount).ok_or(AccessError::Overflow)?; }
and mint it to the appropriate accounts. You can find the full implementation of the claim instruction here.
One thing to note is that Access Protocol V2 doesn't use Anchor. The reason for this is that the program has been created even before the Anchor framework and as V2 was only an extension we decided not to rewrite it fully.

Frontend implementation

As we now have all the on-chain logic in place, we can decide about the most convenient way to implement this on our FE. We do it as follows:
1. Every user has a unique ID that we use to generate the invite link. This link could look like this: https://hubv2.accessprotocol.co/invite/cb7f357bbcae610cd9b5f351824ddd89
2. If someone clicks this link, we retrieve the public key of the inviter from the ID in the link and note it down to the local storage. We then automatically forward the user to the login page.
3. When the new user stakes or creates a pool, we check the local storage and if there is a value there, we append an additional instruction to the transaction that we send to the blockchain. This instruction creates an appropriate royalty account as you've seen in the previous section.
And that's it. There is no additional friction for the invited user.

Conclusion

The newly implemented Royalty Account in Access Protocol V2 enable us to incentivize users to invite their friends and followers to use the platform. Every user that accepts the invitation will then pay 10% royalty for 1 year from their protocol rewards to the person that invited them.
Stay tuned, this is coming to production very soon alongside our brand new application.
Comments
To comment, please sign in.
Article has no comments yet.