diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/CHANGELOG.md b/CHANGELOG.md index e139645..827e688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Update anchor to `1.0.2` +- The `fund_by_claiming_fee` endpoint now rejects when `skip_reward` is non-zero for DAMM v2 `claim_reward` instruction to prevent clearing pending rewards without funding the `FeeVault` ## dynamic-fee-sharing [0.1.1] [PR #8](https://github.com/MeteoraAg/dynamic-fee-sharing/pull/8) diff --git a/programs/dynamic-fee-sharing/src/error.rs b/programs/dynamic-fee-sharing/src/error.rs index 06f8d0e..c4f4f96 100644 --- a/programs/dynamic-fee-sharing/src/error.rs +++ b/programs/dynamic-fee-sharing/src/error.rs @@ -34,4 +34,7 @@ pub enum FeeVaultError { #[msg("Invalid action")] InvalidAction, + + #[msg("Invalid parameters")] + InvalidParameters, } diff --git a/programs/dynamic-fee-sharing/src/instructions/ix_fund_by_claiming_fee.rs b/programs/dynamic-fee-sharing/src/instructions/ix_fund_by_claiming_fee.rs index 4011750..193776e 100644 --- a/programs/dynamic-fee-sharing/src/instructions/ix_fund_by_claiming_fee.rs +++ b/programs/dynamic-fee-sharing/src/instructions/ix_fund_by_claiming_fee.rs @@ -22,7 +22,7 @@ pub struct FundByClaimingFeeCtx<'info> { pub source_program: UncheckedAccount<'info>, } -pub fn is_support_action<'info>( +fn is_support_action<'info>( source_program: &Pubkey, discriminator: &[u8], token_vault: Pubkey, @@ -38,11 +38,29 @@ pub fn is_support_action<'info>( false } +fn validate_payload(source_program: &Pubkey, discriminator: &[u8], payload: &[u8]) -> Result<()> { + if source_program.eq(&damm_v2::ID) + && discriminator == damm_v2::client::args::ClaimReward::DISCRIMINATOR + { + let args = damm_v2::client::args::ClaimReward::try_from_slice(&payload[8..]) + .map_err(|_| FeeVaultError::InvalidAction)?; + + // when skip_reward == 1 and the token_account is frozen, damm_v2 clears the pending reward without transferring it + // so skipping the reward is not allowed when funding the FeeVault + require!(args.skip_reward == 0, FeeVaultError::InvalidParameters); + } + + Ok(()) +} + pub fn handle_fund_by_claiming_fee( ctx: Context, payload: Vec, ) -> Result<()> { + require!(payload.len() >= 8, FeeVaultError::InvalidAction); + let discriminator = &payload[..8]; // first 8 bytes is discriminator + require!( is_support_action( ctx.accounts.source_program.key, @@ -53,6 +71,8 @@ pub fn handle_fund_by_claiming_fee( FeeVaultError::InvalidAction ); + validate_payload(ctx.accounts.source_program.key, &discriminator, &payload)?; + let fee_vault = ctx.accounts.fee_vault.load()?; require!( diff --git a/tests/claim_damm_v2.test.ts b/tests/claim_damm_v2.test.ts index e963aeb..e8ff3cd 100644 --- a/tests/claim_damm_v2.test.ts +++ b/tests/claim_damm_v2.test.ts @@ -7,7 +7,12 @@ import { startSvm, warpToTimestamp, } from "./common/svm"; -import { createToken, getFeeVault, mintToken } from "./common"; +import { + createToken, + getFeeVault, + getProgramErrorCodeHexString, + mintToken, +} from "./common"; import { createDammV2Pool, dammV2Swap, @@ -213,4 +218,66 @@ describe("Fund by claiming damm v2", () => { ); expect(Number(postFeePerShare.sub(preFeePerShare))).gt(0); }); + + it("Reject claiming damm v2 reward with skip_reward set", async () => { + const { feeVault, tokenVault } = await createFeeVaultPda( + svm, + admin, + vaultOwner.publicKey, + rewardMint, + { + padding: [], + users: [ + { + address: shareHolder.publicKey, + share: 100, + }, + { + address: PublicKey.unique(), + share: 100, + }, + ], + } + ); + + const setAuthorityIx = createSetAuthorityInstruction( + positionNftAccount, + creator.publicKey, + AuthorityType.AccountOwner, + feeVault, + [], + TOKEN_2022_PROGRAM_ID + ); + const assignOwnerTx = new Transaction().add(setAuthorityIx); + assignOwnerTx.recentBlockhash = svm.latestBlockhash(); + assignOwnerTx.sign(creator); + + sendTransactionOrExpectThrowError(svm, assignOwnerTx); + + const rewardIndex = 0; + await initializeAndFundReward( + svm, + creator, + dammV2Pool, + rewardMint, + rewardIndex + ); + + warpToTimestamp(svm, new BN(12 * 60 * 60)); + + const errorCode = getProgramErrorCodeHexString("InvalidParameters"); + await claimDammV2Reward( + svm, + shareHolder, + creator, + feeVault, + tokenVault, + dammV2Pool, + position, + positionNftAccount, + rewardIndex, + true, + errorCode + ); + }); }); diff --git a/tests/common/dfs.ts b/tests/common/dfs.ts index 4ba4a80..c0551d7 100644 --- a/tests/common/dfs.ts +++ b/tests/common/dfs.ts @@ -76,7 +76,8 @@ async function fundByClaimingFee( tokenVault: PublicKey, remainingAccounts: AccountMeta[], payload: Buffer, - sourceProgram: PublicKey + sourceProgram: PublicKey, + errorCode?: number ) { const program = createProgram(); @@ -94,7 +95,7 @@ async function fundByClaimingFee( tx.recentBlockhash = svm.latestBlockhash(); tx.sign(signer); - const result = sendTransactionOrExpectThrowError(svm, tx); + const result = sendTransactionOrExpectThrowError(svm, tx, false, errorCode); return result; } @@ -222,7 +223,9 @@ export async function claimDammV2Reward( dammv2Pool: PublicKey, position: PublicKey, positionNftAccount: PublicKey, - rewardIndex: number + rewardIndex: number, + skipReward = false, + errorCode?: number ) { const dammV2PoolState = getDammV2PoolState(svm, dammv2Pool); @@ -292,7 +295,7 @@ export async function claimDammV2Reward( const payload = Buffer.concat([ Buffer.from(claimDammV2RewardDisc), Buffer.from([rewardIndex]), - Buffer.from([1]), + Buffer.from([Number(skipReward)]), ]); await fundByClaimingFee( svm, @@ -301,7 +304,8 @@ export async function claimDammV2Reward( tokenVault, remainingAccounts, payload, - DAMM_V2_PROGRAM_ID + DAMM_V2_PROGRAM_ID, + errorCode ); }