Skip to content

Commit

Permalink
add token transferFee validation on new_distributor
Browse files Browse the repository at this point in the history
  • Loading branch information
makarychev committed Nov 18, 2024
1 parent d693552 commit ea25e10
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 5 deletions.
5 changes: 5 additions & 0 deletions app/src/idls/dividends.json
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,11 @@
"code": 6011,
"name": "ValueUnchanged",
"msg": "The provided value is already set. No changes were made"
},
{
"code": 6012,
"name": "TransferFeeIsNotAllowedForPaymentMint",
"msg": "Transfer fee is not allowed for payment mint"
}
],
"types": [
Expand Down
5 changes: 5 additions & 0 deletions app/src/types/dividends.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,11 @@ export type Dividends = {
code: 6011;
name: "valueUnchanged";
msg: "The provided value is already set. No changes were made";
},
{
code: 6012;
name: "transferFeeIsNotAllowedForPaymentMint";
msg: "Transfer fee is not allowed for payment mint";
}
];
types: [
Expand Down
2 changes: 2 additions & 0 deletions programs/dividends/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ pub enum DividendsErrorCode {
InvalidIPFSHashSize,
#[msg("The provided value is already set. No changes were made")]
ValueUnchanged,
#[msg("Transfer fee is not allowed for payment mint")]
TransferFeeIsNotAllowedForPaymentMint,
}
19 changes: 18 additions & 1 deletion programs/dividends/src/instructions/new_distributor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ use access_control::{
program::AccessControl as AccessControlProgram, AccessControl, WalletRole, ACCESS_CONTROL_SEED,
};
use anchor_lang::{prelude::*, solana_program::program_option::COption};
use anchor_spl::{token_2022::ID as TOKEN_2022_PROGRAM_ID, token_interface::Mint};
use anchor_spl::{
token_2022::spl_token_2022::extension::transfer_fee::TransferFeeConfig,
token_2022::ID as TOKEN_2022_PROGRAM_ID,
token_interface::{get_mint_extension_data, Mint},
};

use crate::{errors::DividendsErrorCode, MerkleDistributor, MAX_IPFS_HASH_LEN};

Expand Down Expand Up @@ -81,6 +85,19 @@ pub fn new_distributor(
if ipfs_hash.len() > MAX_IPFS_HASH_LEN {
return Err(DividendsErrorCode::InvalidIPFSHashSize.into());
}
let mint_data: &AccountInfo = &ctx.accounts.mint.to_account_info();
let transfer_fee_extension = get_mint_extension_data::<TransferFeeConfig>(mint_data);
if let Ok(extension) = transfer_fee_extension {
let clock = Clock::get()?;
let transfer_fee_basis_points = u16::from(
extension
.get_epoch_fee(clock.epoch)
.transfer_fee_basis_points,
) as u128;
if transfer_fee_basis_points > 0 {
return Err(DividendsErrorCode::TransferFeeIsNotAllowedForPaymentMint.into());
}
}

let distributor = &mut ctx.accounts.distributor;

Expand Down
3 changes: 1 addition & 2 deletions programs/dividends/src/states/claim_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ use anchor_lang::prelude::*;
///
/// TODO: this is probably better stored as the node that was verified.
#[account]
#[derive(Default)]
#[derive(InitSpace)]
#[derive(Default, InitSpace)]
pub struct ClaimStatus {
/// If true, the tokens have been claimed.
pub is_claimed: bool,
Expand Down
156 changes: 154 additions & 2 deletions tests/dividends/new-distributor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { AnchorProvider, Program, BN } from "@coral-xyz/anchor";
import { Dividends } from "../../target/types/dividends";
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import { assert } from "chai";
import { assert, expect } from "chai";
import { solToLamports, topUpWallet } from "../utils";
import { toBytes32Array } from "../../app/src/merkle-distributor/utils";
import { createDistributor } from "./utils";
import { createDistributor, createMockTokenWithTransferFee } from "./utils";
import {
TestEnvironment,
TestEnvironmentParams,
} from "../helpers/test_environment";
import { Roles } from "../helpers/access-control_helper";
import { findDistributorKey } from "../../app/src/merkle-distributor";

type TestCase = {
tokenProgramId: PublicKey;
Expand Down Expand Up @@ -263,3 +264,154 @@ testCases.forEach(({ tokenProgramId, programName }) => {
});
});
});

describe("new-distributor with transferFeeConfig", () => {
const provider = AnchorProvider.env();
const connection = provider.connection;
anchor.setProvider(provider);
const commitment = "confirmed";

const dividendsProgram = anchor.workspace.Dividends as Program<Dividends>;
const decimals = 6;

const NUM_NODES = new BN(3);
const TOTAL_CLAIM_AMOUNT = new BN(1_000_000_000_000);
const ZERO_BYTES32 = Buffer.alloc(32);
let distributor: PublicKey;
let bump: number;
let baseKey: Keypair;
let signer;
const root = ZERO_BYTES32;
const totalClaimAmount = TOTAL_CLAIM_AMOUNT;
const numNodes = NUM_NODES;
const ipfsHash =
"QmQ9Q5Q6Q7Q8Q9QaQbQcQdQeQfQgQhQiQjQkQlQmQnQoQpQqQrQsQtQuQvQwQxQy";

const testEnvironmentParams: TestEnvironmentParams = {
mint: {
decimals: decimals,
name: "XYZ Token",
symbol: "XYZ",
uri: "https://example.com",
},
initialSupply: 1_000_000_000_000,
maxHolders: 10000,
maxTotalSupply: 100_000_000_000_000,
};
let testEnvironment: TestEnvironment;

beforeEach(async () => {
testEnvironment = new TestEnvironment(testEnvironmentParams);
await testEnvironment.setupAccessControl();
signer = testEnvironment.contractAdmin;

await topUpWallet(connection, signer.publicKey, solToLamports(1));
});

it("fails to initialize new distributor with non-zero transferFee", async () => {
const feeBasicPoints = 1000;
const paymentMintObjects = await createMockTokenWithTransferFee(
connection,
decimals,
feeBasicPoints
);
baseKey = Keypair.generate();
[distributor, bump] = findDistributorKey(
baseKey.publicKey,
dividendsProgram.programId
);

try {
await dividendsProgram.methods
.newDistributor(
bump,
toBytes32Array(root),
totalClaimAmount,
numNodes,
ipfsHash
)
.accountsStrict({
base: baseKey.publicKey,
distributor,
mint: paymentMintObjects.mint,
authorityWalletRole:
testEnvironment.accessControlHelper.walletRolePDA(
signer.publicKey
)[0],
accessControl:
testEnvironment.accessControlHelper.accessControlPubkey,
securityMint: testEnvironment.mintKeypair.publicKey,
payer: signer.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([signer, baseKey])
.rpc({ commitment });
assert.fail("Expected to throw an error");
} catch ({ error }) {
assert.equal(error.errorCode.code, "TransferFeeIsNotAllowedForPaymentMint");
assert.equal(error.errorMessage, "Transfer fee is not allowed for payment mint");
}
});

it("initializes new distributor with zero transferFee", async () => {
const feeBasicPoints = 0;
const { mint: paymentMintPubkey } = await createMockTokenWithTransferFee(
connection,
decimals,
feeBasicPoints
);
baseKey = Keypair.generate();
[distributor, bump] = findDistributorKey(
baseKey.publicKey,
dividendsProgram.programId
);

await dividendsProgram.methods
.newDistributor(
bump,
toBytes32Array(root),
totalClaimAmount,
numNodes,
ipfsHash
)
.accountsStrict({
base: baseKey.publicKey,
distributor,
mint: paymentMintPubkey,
authorityWalletRole:
testEnvironment.accessControlHelper.walletRolePDA(
signer.publicKey
)[0],
accessControl:
testEnvironment.accessControlHelper.accessControlPubkey,
securityMint: testEnvironment.mintKeypair.publicKey,
payer: signer.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([signer, baseKey])
.rpc({ commitment });

const distributorData =
await dividendsProgram.account.merkleDistributor.fetch(distributor);
assert.equal(distributorData.bump, bump);
assert.equal(distributorData.numNodes.toString(), NUM_NODES.toString());
assert.equal(
distributorData.totalClaimAmount.toString(),
TOTAL_CLAIM_AMOUNT.toString()
);
assert.deepEqual(distributorData.base, baseKey.publicKey);
assert.deepEqual(distributorData.mint, paymentMintPubkey);
assert.deepEqual(
distributorData.accessControl,
testEnvironment.accessControlHelper.accessControlPubkey
);
assert.isFalse(distributorData.paused);
assert.equal(distributorData.numNodesClaimed.toNumber(), 0);
assert.deepEqual(
distributorData.root,
Array.from(new Uint8Array(ZERO_BYTES32))
);
assert.equal(distributorData.totalAmountClaimed.toNumber(), 0);
assert.isFalse(distributorData.readyToClaim);
});
});

0 comments on commit ea25e10

Please sign in to comment.