Skip to content
Open
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
4 changes: 3 additions & 1 deletion programs/futarchy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub enum FutarchyError {
PassThresholdTooHigh,
#[msg("Question must have exactly 2 outcomes for binary futarchy")]
QuestionMustBeBinary,
#[msg("Squads proposal must be in Draft status")]
#[msg("Squads proposal must be in Active status")]
InvalidSquadsProposalStatus,
#[msg("Casting overflow. If you're seeing this, please report this")]
CastingOverflow,
Expand Down Expand Up @@ -76,4 +76,6 @@ pub enum FutarchyError {
InvalidTargetK,
#[msg("Failed to compile transaction message for Squads vault transaction")]
InvalidTransactionMessage,
#[msg("Base mint and quote mint must be different")]
InvalidMint,
}
6 changes: 3 additions & 3 deletions programs/futarchy/src/instructions/finalize_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ impl FinalizeProposal<'_> {
];
let proposal_signer = &[&proposal_seeds[..]];

let clock = Clock::get()?;

let calculate_twap = |amm: &Pool| -> Result<u128> {
let seconds_passed = amm.oracle.last_updated_timestamp - proposal.timestamp_enqueued;

Expand All @@ -124,7 +126,7 @@ impl FinalizeProposal<'_> {
FutarchyError::MarketsTooYoung
);

amm.get_twap()
amm.get_twap(clock.unix_timestamp)
};

let PoolState::Futarchy {
Expand Down Expand Up @@ -269,8 +271,6 @@ impl FinalizeProposal<'_> {

dao.seq_num += 1;

let clock = Clock::get()?;

emit_cpi!(FinalizeProposalEvent {
common: CommonFields::new(&clock, dao.seq_num),
proposal: proposal.key(),
Expand Down
9 changes: 9 additions & 0 deletions programs/futarchy/src/instructions/initialize_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ pub mod permissionless_account {
}

impl InitializeDao<'_> {
pub fn validate(&self) -> Result<()> {
require_keys_neq!(
self.base_mint.key(),
self.quote_mint.key(),
FutarchyError::InvalidMint
);
Ok(())
}

pub fn handle(ctx: Context<Self>, params: InitializeDaoParams) -> Result<()> {
let InitializeDaoParams {
twap_initial_observation,
Expand Down
1 change: 1 addition & 0 deletions programs/futarchy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub const DEFAULT_MAX_OBSERVATION_CHANGE_PER_UPDATE_LOTS: u64 = 5_000;
pub mod futarchy {
use super::*;

#[access_control(ctx.accounts.validate())]
pub fn initialize_dao(ctx: Context<InitializeDao>, params: InitializeDaoParams) -> Result<()> {
InitializeDao::handle(ctx, params)
}
Expand Down
14 changes: 10 additions & 4 deletions programs/futarchy/src/state/futarchy_amm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ impl Pool {

// if this saturates, the aggregator will wrap back to 0, so this value doesn't
// really matter. we just can't panic.
let weighted_observation = new_observation.saturating_mul(time_difference);
let weighted_observation = last_observation.saturating_mul(time_difference);

oracle.aggregator.wrapping_add(weighted_observation)
};
Expand Down Expand Up @@ -449,17 +449,23 @@ impl Pool {
}

/// Returns the time-weighted average price since market creation
pub fn get_twap(&self) -> Result<u128> {
pub fn get_twap(&self, current_timestamp: i64) -> Result<u128> {
let start_timestamp =
self.oracle.created_at_timestamp + self.oracle.start_delay_seconds as i64;

require_gt!(self.oracle.last_updated_timestamp, start_timestamp);
let seconds_passed = (self.oracle.last_updated_timestamp - start_timestamp) as u128;

let seconds_passed = (current_timestamp - start_timestamp) as u128;

require_neq!(seconds_passed, 0);
require_neq!(self.oracle.aggregator, 0);

Ok(self.oracle.aggregator / seconds_passed)
// include the final interval that hasn't been accumulated yet
let final_interval = (current_timestamp - self.oracle.last_updated_timestamp) as u128;
let final_contribution = self.oracle.last_observation.saturating_mul(final_interval);
let total_aggregator = self.oracle.aggregator.wrapping_add(final_contribution);

Ok(total_aggregator / seconds_passed)
}
}

Expand Down
2 changes: 2 additions & 0 deletions programs/price_based_performance_package/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ pub enum PriceBasedPerformancePackageError {
InvalidAdmin,
#[msg("Total token amount calculation would overflow")]
TotalTokenAmountOverflow,
#[msg("Recipient and performance package authority must be different keys")]
RecipientAuthorityMustDiffer,
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ pub struct ChangePerformancePackageAuthority<'info> {
}

impl<'info> ChangePerformancePackageAuthority<'info> {
pub fn validate(&self, params: &ChangePerformancePackageAuthorityParams) -> Result<()> {
require_keys_neq!(
params.new_performance_package_authority,
self.performance_package.recipient,
PriceBasedPerformancePackageError::RecipientAuthorityMustDiffer
);

Ok(())
}

pub fn handle(
ctx: Context<Self>,
params: ChangePerformancePackageAuthorityParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ impl<'info> ExecuteChange<'info> {
return Err(PriceBasedPerformancePackageError::UnauthorizedChangeRequest.into());
}

if let ChangeType::Recipient { new_recipient } = &self.change_request.change_type {
require_keys_neq!(
*new_recipient,
self.performance_package.performance_package_authority,
PriceBasedPerformancePackageError::RecipientAuthorityMustDiffer
);
}

Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ pub struct InitializePerformancePackage<'info> {
}

impl InitializePerformancePackage<'_> {
pub fn validate(&self, params: &InitializePerformancePackageParams) -> Result<()> {
require_keys_neq!(
params.grantee,
params.performance_package_authority,
PriceBasedPerformancePackageError::RecipientAuthorityMustDiffer
);

Ok(())
}

pub fn handle(ctx: Context<Self>, params: InitializePerformancePackageParams) -> Result<()> {
let Self {
performance_package,
Expand Down
2 changes: 2 additions & 0 deletions programs/price_based_performance_package/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ declare_id!("pbPPQH7jyKoSLu8QYs3rSY3YkDRXEBojKbTgnUg7NDS");
pub mod price_based_performance_package {
use super::*;

#[access_control(ctx.accounts.validate(&params))]
pub fn initialize_performance_package(
ctx: Context<InitializePerformancePackage>,
params: InitializePerformancePackageParams,
Expand Down Expand Up @@ -66,6 +67,7 @@ pub mod price_based_performance_package {
ExecuteChange::handle(ctx)
}

#[access_control(ctx.accounts.validate(&params))]
pub fn change_performance_package_authority(
ctx: Context<ChangePerformancePackageAuthority>,
params: ChangePerformancePackageAuthorityParams,
Expand Down
14 changes: 12 additions & 2 deletions sdk/src/v0.7/types/futarchy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3146,7 +3146,7 @@ export type Futarchy = {
{
code: 6014;
name: "InvalidSquadsProposalStatus";
msg: "Squads proposal must be in Draft status";
msg: "Squads proposal must be in Active status";
},
{
code: 6015;
Expand Down Expand Up @@ -3253,6 +3253,11 @@ export type Futarchy = {
name: "InvalidTransactionMessage";
msg: "Failed to compile transaction message for Squads vault transaction";
},
{
code: 6036;
name: "InvalidMint";
msg: "Base mint and quote mint must be different";
},
];
};

Expand Down Expand Up @@ -6404,7 +6409,7 @@ export const IDL: Futarchy = {
{
code: 6014,
name: "InvalidSquadsProposalStatus",
msg: "Squads proposal must be in Draft status",
msg: "Squads proposal must be in Active status",
},
{
code: 6015,
Expand Down Expand Up @@ -6511,5 +6516,10 @@ export const IDL: Futarchy = {
name: "InvalidTransactionMessage",
msg: "Failed to compile transaction message for Squads vault transaction",
},
{
code: 6036,
name: "InvalidMint",
msg: "Base mint and quote mint must be different",
},
],
};
10 changes: 10 additions & 0 deletions sdk/src/v0.7/types/price_based_performance_package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,11 @@ export type PriceBasedPerformancePackage = {
name: "TotalTokenAmountOverflow";
msg: "Total token amount calculation would overflow";
},
{
code: 6015;
name: "RecipientAuthorityMustDiffer";
msg: "Recipient and performance package authority must be different keys";
},
];
};

Expand Down Expand Up @@ -1947,5 +1952,10 @@ export const IDL: PriceBasedPerformancePackage = {
name: "TotalTokenAmountOverflow",
msg: "Total token amount calculation would overflow",
},
{
code: 6015,
name: "RecipientAuthorityMustDiffer",
msg: "Recipient and performance package authority must be different keys",
},
],
};
28 changes: 28 additions & 0 deletions tests/futarchy/unit/initializeDao.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,34 @@ export default function suite() {
assert.equal(storedSpendingLimit.destinations.length, 0);
});

it("doesn't allow DAOs with identical base and quote mints", async function () {
const SAME_MINT = await this.createMint(this.payer.publicKey, 6);

const callbacks = expectError(
"InvalidMint",
"DAO initialized despite base and quote mints being identical",
);

await this.futarchy
.initializeDaoIx({
baseMint: SAME_MINT,
quoteMint: SAME_MINT,
params: {
secondsPerProposal: 60 * 60 * 24 * 3,
twapStartDelaySeconds: 60 * 60 * 24,
twapInitialObservation: THOUSAND_BUCK_PRICE,
twapMaxObservationChangePerUpdate: THOUSAND_BUCK_PRICE.divn(100),
minQuoteFutarchicLiquidity: new BN(1),
minBaseFutarchicLiquidity: new BN(1000),
passThresholdBps: 300,
nonce: new BN(9999),
initialSpendingLimit: null,
},
})
.rpc()
.then(callbacks[0], callbacks[1]);
});

it("doesn't allow DAOs with proposal duration less than TWAP start delay", async function () {
const callbacks = expectError(
"ProposalDurationTooShort",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@solana/web3.js";
import { assert } from "chai";
import BN from "bn.js";
import { expectError } from "../../utils.js";

export default function () {
let createKey: Keypair;
Expand Down Expand Up @@ -90,6 +91,22 @@ export default function () {
.rpc();
});

it("should fail if new authority equals current recipient", async function () {
const callbacks = expectError(
"RecipientAuthorityMustDiffer",
"Recipient and performance package authority must be different keys",
);

await this.priceBasedPerformancePackage
.changePerformancePackageAuthorityIx({
performancePackage,
currentAuthority: this.payer.publicKey,
newPerformancePackageAuthority: recipient.publicKey,
})
.rpc()
.then(callbacks[0], callbacks[1]);
});

it("should fail if unauthorized party tries to change authority", async function () {
const unauthorizedWallet = Keypair.generate();

Expand Down
41 changes: 41 additions & 0 deletions tests/priceBasedPerformancePackage/unit/executeChange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,47 @@ export default function () {
.rpc();
});

it("should fail if new recipient equals current authority", async function () {
const pdaNonce = Math.floor(Math.random() * 1000000);

// Recipient proposes changing recipient to authority's key (this.payer.publicKey)
await this.priceBasedPerformancePackage
.proposeChangeIx({
params: {
changeType: {
recipient: { newRecipient: this.payer.publicKey },
},
pdaNonce,
},
performancePackage,
proposer: recipient.publicKey,
})
.signers([recipient])
.rpc();

const changeRequestAddr =
this.priceBasedPerformancePackage.getChangeRequestAddress(
performancePackage,
recipient.publicKey,
pdaNonce,
);

const callbacks = expectError(
"RecipientAuthorityMustDiffer",
"Recipient and performance package authority must be different keys",
);

// Authority executes - should fail because new recipient == authority
await this.priceBasedPerformancePackage
.executeChangeIx({
performancePackage,
changeRequest: changeRequestAddr,
executor: this.payer.publicKey,
})
.rpc()
.then(callbacks[0], callbacks[1]);
});

it("should fail if wrong vault tries to execute", async function () {
// Recipient proposes change (correct pattern)
const pdaNonce = Math.floor(Math.random() * 1000000);
Expand Down
Loading
Loading