Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 44 additions & 0 deletions tokens/token-2022/transfer-hook/allow-block-list-token/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# Anchor
/anchor/target/
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Add files here to ignore them from prettier formatting
/anchor/target/debug
/anchor/target/deploy
/anchor/target/release
/anchor/target/sbf-solana-solana
/anchor/target/test-ledger
/anchor/target/.rustc_info.json
/dist
/coverage
.next
/tmp
package-lock.json
pnpm-lock.yaml
yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"arrowParens": "always",
"printWidth": 120,
"semi": false,
"singleQuote": true,
"trailingComma": "all"
}
50 changes: 50 additions & 0 deletions tokens/token-2022/transfer-hook/allow-block-list-token/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# AllowBlockList Token

An example of a allow / block list token using token extensions.

## Features

Allows the creation of an allow block list with a list authority.
The allow/block list is then consumed by a transfer-hook.

The list is managed by a single authority and can be used by several token mints. This enables a separation of concerns between token management and allow/block list management, ideal for scenarios where an issuer wants a 3rd party managed allow/block list or wants to share the same list across a group of assets.

Initializes new tokens with several configuration options:
- Permanent delegate
- Allow list
- Block list
- Metadata
- Authorities

The issuer can configure the allow and block list with 3 distinct configurations:
- Force Allow: requires everyone receiving tokens to be explicitly allowed in
- Block: allows everyone to receive tokens unless explicitly blocked
- Threshold Allow: allows everyone to receive tokens unless explicitly blocked up until a given transfer amount threshold. Transfers larger than the threshold require explicitly allow

These configurations are saved in the token mint metadata.

This repo includes a UI to manage the allow/block list based on the `legacy-next-tailwind-basic` template. It also allows creating new token mints on the spot with transfer-hook enabled along with token transfers given that most wallets fail to fetch transfer-hook dependencies on devnet and locally.

## Setup

Install dependencies:
`yarn install`

Compile the program:
`anchor build`

Compile the UI:
`yarn run build`

Serve the UI:
`yarn run dev`

### Local testing

There are a couple scripts to manage the local validator and deployment.

To start the local validator and deploy the program:
`./scripts/start.sh`

To stop the local validator:
`./scripts/stop.sh`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.anchor
.DS_Store
target/debug
target/deploy
target/release
target/sbf-solana-solana
target/test-ledger
target/.rustc_info.json
**/*.rs.bk
node_modules
test-ledger
.yarn
ledger
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.anchor
.DS_Store
target
node_modules
dist
build
test-ledger
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[toolchain]
package_manager = "yarn"

[features]
resolution = true
skip-lint = false

[programs.localnet]
abl-token = "LtkoMwPSKxAE714EY3V1oAEQ5LciqJcRwQQuQnzEhQQ"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "../node_modules/.bin/jest --preset ts-jest"
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[workspace]
members = [
"programs/*"
]
resolver = "2"

[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1
[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[package]
name = "abl-token"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "abl_token"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]


[dependencies]
anchor-lang = { version = "0.31.1", features = ["interface-instructions"] }
anchor-spl = { version = "0.31.1", features = [
"token_2022_extensions",
"token_2022",
] }


spl-tlv-account-resolution = "0.8.1"
spl-transfer-hook-interface = { version = "0.8.2" }
spl-discriminator = "0.3"

[dev-dependencies]
litesvm = "0.6.1"


solana-instruction = "2.2.1"
solana-keypair = "2.2.1"
solana-native-token = "2.2.1"
solana-pubkey = "2.2.1"
solana-signer = "2.2.1"
solana-system-interface = "1.0.0"
solana-transaction = "2.2.1"
solana-message = "2.2.1"
solana-sdk-ids = "2.2.1"
spl-token-2022 = { version = "8.0.1", features = ["no-entrypoint"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

pub const META_LIST_ACCOUNT_SEED: &[u8] = b"extra-account-metas";
pub const CONFIG_SEED: &[u8] = b"config";
pub const AB_WALLET_SEED: &[u8] = b"ab_wallet";
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use anchor_lang::error_code;

#[error_code]
pub enum ABListError {
#[msg("Invalid metadata")]
InvalidMetadata,

#[msg("Wallet not allowed")]
WalletNotAllowed,

#[msg("Amount not allowed")]
AmountNotAllowed,

#[msg("Wallet blocked")]
WalletBlocked,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use anchor_lang::{prelude::*, solana_program::program::invoke, solana_program::system_instruction::transfer};
use anchor_spl::{
token_2022::{spl_token_2022::{extension::{BaseStateWithExtensions, StateWithExtensions}, state::Mint as Mint2022}, Token2022},
token_interface::{spl_token_metadata_interface::state::{Field, TokenMetadata}, token_metadata_initialize, token_metadata_update_field, Mint, TokenMetadataInitialize, TokenMetadataUpdateField},
};

use spl_tlv_account_resolution::{
state::ExtraAccountMetaList,
};
use spl_transfer_hook_interface::instruction::ExecuteInstruction;

use crate::{Mode, META_LIST_ACCOUNT_SEED, get_extra_account_metas, get_meta_list_size};


#[derive(Accounts)]
#[instruction(args: AttachToMintArgs)]
pub struct AttachToMint<'info> {
#[account(mut)]
pub payer: Signer<'info>,

pub mint_authority: Signer<'info>,

pub metadata_authority: Signer<'info>,

#[account(
mut,
mint::token_program = token_program,
)]
pub mint: Box<InterfaceAccount<'info, Mint>>,

#[account(
init,
space = get_meta_list_size()?,
seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()],
bump,
payer = payer,
)]
/// CHECK: extra metas account
pub extra_metas_account: UncheckedAccount<'info>,

pub system_program: Program<'info, System>,

pub token_program: Program<'info, Token2022>,
}

impl AttachToMint<'_> {
pub fn attach_to_mint(&mut self, args: AttachToMintArgs) -> Result<()> {
let mint_info = self.mint.to_account_info();
let mint_data = mint_info.data.borrow();
let mint = StateWithExtensions::<Mint2022>::unpack(&mint_data)?;

let metadata = mint.get_variable_len_extension::<TokenMetadata>();

if metadata.is_err() {
// assume metadata is not initialized, so we need to initialize it

let cpi_accounts = TokenMetadataInitialize {
program_id: self.token_program.to_account_info(),
mint: self.mint.to_account_info(),
metadata: self.mint.to_account_info(), // metadata account is the mint, since data is stored in mint
mint_authority: self.mint_authority.to_account_info(),
update_authority: self.metadata_authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(
self.token_program.to_account_info(),
cpi_accounts,
);
token_metadata_initialize(cpi_ctx, args.name.unwrap(), args.symbol.unwrap(), args.uri.unwrap())?;
}

let cpi_accounts = TokenMetadataUpdateField {
metadata: self.mint.to_account_info(),
update_authority: self.metadata_authority.to_account_info(),
program_id: self.token_program.to_account_info(),
};

let cpi_ctx = CpiContext::new(
self.token_program.to_account_info(),
cpi_accounts,
);

token_metadata_update_field(cpi_ctx, Field::Key("AB".to_string()), args.mode.to_string())?;

if args.mode == Mode::Mixed {
let cpi_accounts = TokenMetadataUpdateField {
metadata: self.mint.to_account_info(),
update_authority: self.metadata_authority.to_account_info(),
program_id: self.token_program.to_account_info(),
};
let cpi_ctx = CpiContext::new(
self.token_program.to_account_info(),
cpi_accounts,
);

token_metadata_update_field(
cpi_ctx,
Field::Key("threshold".to_string()),
args.threshold.to_string(),
)?;
}


let data = self.mint.to_account_info().data_len();
let min_balance = Rent::get()?.minimum_balance(data);
if min_balance > self.mint.to_account_info().get_lamports() {
invoke(
&transfer(
&self.payer.key(),
&self.mint.to_account_info().key(),
min_balance - self.mint.to_account_info().get_lamports(),
),
&[
self.payer.to_account_info(),
self.mint.to_account_info(),
self.system_program.to_account_info(),
],
)?;
}

// initialize the extra metas account
let extra_metas_account = &self.extra_metas_account;
let metas = get_extra_account_metas()?;
let mut data = extra_metas_account.try_borrow_mut_data()?;
ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &metas)?;

Ok(())

}
}

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct AttachToMintArgs {
pub name: Option<String>,
pub symbol: Option<String>,
pub uri: Option<String>,
pub mode: Mode,
pub threshold: u64,
}

Loading
Loading