Skip to content

Commit 71aabfe

Browse files
committed
fix size in collection config
1 parent a31675d commit 71aabfe

9 files changed

Lines changed: 61 additions & 55 deletions

File tree

programs/core-attribute-voter/README.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Core Attribute Voter allows DAOs to use NFT collections for governance voting wh
1212

1313
Each configured collection specifies:
1414
- A **weight attribute key** (e.g. `"voting_power"`, `"tier"`) — the attribute name to read from each NFT
15-
- A **max weight** — a ceiling that caps any single NFT's voting power
15+
- A **max weight** — a ceiling that caps any single NFT's voting power and represents the collection's max governance weight
1616
- An **expected attribute authority** — the trusted authority that set the attributes
1717

1818
When a voter submits their NFTs, the program:
@@ -32,10 +32,39 @@ When a voter submits their NFTs, the program:
3232
The maximum possible voting power across all configured collections:
3333

3434
```
35-
max_voter_weight = Σ (collection.size × collection.max_weight)
35+
max_voter_weight = Σ collection.max_weight
3636
```
3737

38-
This is used by SPL Governance to calculate quorum thresholds.
38+
This is used by SPL Governance to calculate quorum thresholds. Unlike the nft-voter and core-voter plugins (where max weight = `collection_size × weight_per_nft`), attribute-based voting has variable per-NFT weights, so `max_weight` must be set by the realm authority to reflect the expected total voting power of each collection.
39+
40+
#### Setting `max_weight` correctly
41+
42+
`max_weight` plays a dual role — it caps individual NFT weights **and** feeds into the quorum denominator. Getting it right matters:
43+
44+
**Example 1 — Well-calibrated:**
45+
A collection of 50 NFTs where attributes range from 1–10, totalling ~200 across the collection.
46+
Setting `max_weight = 200` means:
47+
- Individual NFTs are capped at 200 (effectively uncapped since max attribute is 10)
48+
- Quorum denominator reflects the true total voting power
49+
- A 60% quorum requires 120 voting power to pass
50+
51+
**Example 2 — Set too low:**
52+
Same collection, but `max_weight = 50`.
53+
- Individual NFTs with attribute > 50 get capped (unlikely here, but enforced)
54+
- Quorum denominator is only 50, so just 30 voting power (60% quorum) passes a proposal
55+
- Risk: a small minority of NFT holders can pass proposals
56+
57+
**Example 3 — Set too high:**
58+
Same collection, but `max_weight = 10000`.
59+
- No individual capping (attributes are far below 10000)
60+
- Quorum denominator is 10000, so 6000 voting power needed for 60% quorum
61+
- Risk: quorum becomes unreachable since the collection only holds ~200 total power
62+
63+
**Example 4 — Multiple collections:**
64+
Collection A: `max_weight = 500`, Collection B: `max_weight = 300`.
65+
- `max_voter_weight = 500 + 300 = 800`
66+
- A voter holding NFTs from both collections accumulates weight across them
67+
- 60% quorum requires 480 total voting power
3968

4069
## Architecture
4170

@@ -133,7 +162,7 @@ relinquish_nft_vote()
133162

134163
| Parameter | Type | Constraints | Description |
135164
|---|---|---|---|
136-
| `max_weight` | `u64` | Any value | Per-NFT weight ceiling |
165+
| `max_weight` | `u64` | > 0 | Max governance weight for the collection. Caps individual NFT weights and is summed across collections for quorum calculation. Should reflect the expected total voting power of the collection. |
137166
| `weight_attribute_key` | `String` | 1–32 characters | Attribute name to read from NFTs |
138167
| `expected_attribute_authority` | `PluginAuthority` | Must match plugin | Trusted authority for attribute validation |
139168

programs/core-attribute-voter/src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ pub enum CoreNftAttributeVoterError {
99
#[msg("Invalid Realm for Registrar")]
1010
InvalidRealmForRegistrar,
1111

12-
#[msg("Invalid Collection Size")]
13-
InvalidCollectionSize,
12+
#[msg("Invalid max weight, must be greater than 0")]
13+
InvalidMaxWeight,
1414

1515
#[msg("Invalid MaxVoterWeightRecord Realm")]
1616
InvalidMaxVoterWeightRecordRealm,

programs/core-attribute-voter/src/instructions/configure_collection.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub fn configure_collection(
5252
weight_attribute_key: String,
5353
expected_attribute_authority: PluginAuthority,
5454
) -> Result<()> {
55+
let collection_key = ctx.accounts.collection.key();
5556
let registrar = &mut ctx.accounts.registrar;
5657

5758
let realm = realm::get_realm_data_for_governing_token_mint(
@@ -65,33 +66,29 @@ pub fn configure_collection(
6566
CoreNftAttributeVoterError::InvalidRealmAuthority
6667
);
6768

69+
require!(
70+
max_weight > 0,
71+
CoreNftAttributeVoterError::InvalidMaxWeight
72+
);
73+
6874
// Validate weight_attribute_key
6975
require!(
7076
!weight_attribute_key.is_empty() && weight_attribute_key.len() <= 32,
7177
CoreNftAttributeVoterError::InvalidWeightAttributeKey
7278
);
7379

74-
let collection = &ctx.accounts.collection;
75-
76-
let size = collection.current_size;
77-
78-
msg!("Collection size: {}", size);
79-
80-
require!(size > 0, CoreNftAttributeVoterError::InvalidCollectionSize);
81-
8280
let collection_config = CollectionConfig {
83-
collection: collection.key(),
81+
collection: collection_key,
8482
max_weight,
8583
weight_attribute_key,
8684
expected_attribute_authority,
8785
reserved: [0; 8],
88-
size,
8986
};
9087

9188
let collection_idx = registrar
9289
.collection_configs
9390
.iter()
94-
.position(|cc| cc.collection == collection.key());
91+
.position(|cc| cc.collection == collection_key);
9592

9693
if let Some(collection_idx) = collection_idx {
9794
registrar.collection_configs[collection_idx] = collection_config;

programs/core-attribute-voter/src/state/collection_config.rs

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
use anchor_lang::prelude::*;
22
use mpl_core::types::PluginAuthority;
33

4-
use crate::error::CoreNftAttributeVoterError;
5-
64
/// Configuration of an NFT collection used for attribute-based governance power
75
#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, PartialEq)]
86
pub struct CollectionConfig {
97
/// The NFT collection used for governance
108
pub collection: Pubkey,
119

12-
/// The size of the NFT collection used to calculate max voter weight
13-
pub size: u32,
14-
15-
/// Maximum governance power weight of the collection (ceiling for max voter weight calculation)
16-
/// Note: The weight is scaled accordingly to the governing_token_mint decimals
10+
/// Maximum governance power weight of the collection
11+
/// Serves as both the per-NFT weight cap and the quorum denominator contribution
12+
/// (i.e. it is summed across collections to produce MaxVoterWeightRecord.max_voter_weight)
13+
/// Should be set to the expected total voting power of the collection,
14+
/// in the same unit/scale as the attribute values stored on the NFTs
1715
pub max_weight: u64,
1816

1917
/// The attribute key to read the voting weight from on each NFT
@@ -29,21 +27,15 @@ pub struct CollectionConfig {
2927
}
3028

3129
impl CollectionConfig {
32-
/// Borsh serialized size: 32 (Pubkey) + 4 (u32) + 8 (u64) + 4+32 (String with max 32 chars) + 33 (PluginAuthority) + 8 (reserved)
33-
pub const SERIALIZED_SIZE: usize = 32 + 4 + 8 + 36 + 33 + 8;
30+
/// Borsh serialized size: 32 (Pubkey) + 8 (u64) + 4+32 (String with max 32 chars) + 33 (PluginAuthority) + 8 (reserved)
31+
pub const SERIALIZED_SIZE: usize = 32 + 8 + 36 + 33 + 8;
3432

35-
pub fn get_max_weight(&self) -> Result<u64> {
36-
(self.size as u64)
37-
.checked_mul(self.max_weight)
38-
.ok_or_else(|| CoreNftAttributeVoterError::ArithmeticOverflow.into())
39-
}
4033
}
4134

4235
impl Default for CollectionConfig {
4336
fn default() -> Self {
4437
Self {
4538
collection: Pubkey::default(),
46-
size: 0,
4739
max_weight: 0,
4840
// Default to a 32-byte zero-padded string for deterministic sizing
4941
weight_attribute_key: "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0".to_string(),

programs/core-attribute-voter/src/state/registrar.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ impl Registrar {
6868
self.collection_configs
6969
.iter()
7070
.try_fold(0u64, |sum, cc| {
71-
let weight = cc.get_max_weight()?;
72-
sum.checked_add(weight)
71+
sum.checked_add(cc.max_weight)
7372
.ok_or_else(|| CoreNftAttributeVoterError::ArithmeticOverflow.into())
7473
})
7574
}

programs/core-attribute-voter/tests/configure_collection.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ async fn test_configure_collection() -> Result<(), TransportError> {
6262
assert_eq!(max_voter_weight_record.max_voter_weight_expiry, None);
6363
assert_eq!(
6464
max_voter_weight_record.max_voter_weight,
65-
(registrar.collection_configs[0].max_weight as u32 * registrar.collection_configs[0].size)
66-
as u64
65+
registrar.collection_configs[0].max_weight
6766
);
6867

6968
Ok(())
@@ -132,7 +131,7 @@ async fn test_configure_multiple_collections() -> Result<(), TransportError> {
132131
.await;
133132

134133
assert_eq!(max_voter_weight_record.max_voter_weight_expiry, None);
135-
assert_eq!(max_voter_weight_record.max_voter_weight, 25);
134+
assert_eq!(max_voter_weight_record.max_voter_weight, 3);
136135

137136
Ok(())
138137
}
@@ -182,7 +181,7 @@ async fn test_configure_max_collections() -> Result<(), TransportError> {
182181
.await;
183182

184183
assert_eq!(max_voter_weight_record.max_voter_weight_expiry, None);
185-
assert_eq!(max_voter_weight_record.max_voter_weight, 30);
184+
assert_eq!(max_voter_weight_record.max_voter_weight, 10);
186185

187186
Ok(())
188187
}
@@ -237,7 +236,7 @@ async fn test_configure_existing_collection() -> Result<(), TransportError> {
237236
.await;
238237

239238
assert_eq!(max_voter_weight_record.max_voter_weight_expiry, None);
240-
assert_eq!(max_voter_weight_record.max_voter_weight, 20);
239+
assert_eq!(max_voter_weight_record.max_voter_weight, 2);
241240

242241
Ok(())
243242
}

programs/core-attribute-voter/tests/program_test/core_voter_test.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -553,13 +553,8 @@ impl CoreVoterTest {
553553
.process_transaction(&[configure_collection_ix], Some(signers))
554554
.await?;
555555

556-
let collection_account = self
557-
.get_collection_account(&collection_cookie.collection)
558-
.await;
559-
560556
let collection_config = CollectionConfig {
561557
collection: collection_cookie.collection,
562-
size: collection_account.current_size,
563558
max_weight: args.max_weight,
564559
weight_attribute_key: args.weight_attribute_key,
565560
expected_attribute_authority: args.expected_attribute_authority,

programs/core-attribute-voter/tests/relinquish_nft_vote.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async fn test_relinquish_nft_vote() -> Result<(), TransportError> {
3838
&registrar_cookie,
3939
&collection_cookie,
4040
&max_voter_weight_record_cookie,
41-
Some(ConfigureCollectionArgs { max_weight: 1, ..Default::default() }), // Set Size == 1 to complete voting with just one vote
41+
Some(ConfigureCollectionArgs { max_weight: 1, ..Default::default() }), // max_weight == 1 to complete voting with just one vote
4242
)
4343
.await?;
4444

@@ -239,8 +239,7 @@ async fn test_relinquish_nft_vote_for_proposal_in_voting_state_and_vote_record_e
239239
&registrar_cookie,
240240
&collection_cookie,
241241
&max_voter_weight_record_cookie,
242-
None
243-
242+
Some(ConfigureCollectionArgs { max_weight: 5, ..Default::default() }), // max_weight > vote weight so proposal stays in Voting
244243
)
245244
.await?;
246245

@@ -285,8 +284,6 @@ async fn test_relinquish_nft_vote_for_proposal_in_voting_state_and_vote_record_e
285284
.err()
286285
.unwrap();
287286

288-
println!("{:?}", err);
289-
290287
// Assert
291288
assert_nft_voter_err(err, CoreNftAttributeVoterError::VoteRecordMustBeWithdrawn);
292289

@@ -323,7 +320,7 @@ async fn test_relinquish_nft_vote_with_invalid_voter_error() -> Result<(), Trans
323320
&registrar_cookie,
324321
&collection_cookie,
325322
&max_voter_weight_record_cookie,
326-
Some(ConfigureCollectionArgs { max_weight: 1, ..Default::default() }), // Set Size == 1 to complete voting with just one vote
323+
Some(ConfigureCollectionArgs { max_weight: 1, ..Default::default() }), // max_weight == 1 to complete voting with just one vote
327324
)
328325
.await?;
329326

@@ -591,7 +588,7 @@ async fn test_relinquish_nft_vote_using_delegate() -> Result<(), TransportError>
591588
&registrar_cookie,
592589
&collection_cookie,
593590
&max_voter_weight_record_cookie,
594-
Some(ConfigureCollectionArgs { max_weight: 1, ..Default::default() }), // Set Size == 1 to complete voting with just one vote
591+
Some(ConfigureCollectionArgs { max_weight: 1, ..Default::default() }), // max_weight == 1 to complete voting with just one vote
595592
)
596593
.await?;
597594

programs/core-attribute-voter/tests/update_max_voter_weight_record.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ async fn test_update_collection_config_invalidates_max_voter_weight_record_expir
8383
let _clock = core_voter_test.bench.get_clock().await;
8484

8585
// Assert
86-
let max_voter_weight_total =
87-
(collection_1_weight * collection_1_size) + (collection_2_weight * collection_2_size);
86+
let max_voter_weight_total = collection_1_weight + collection_2_weight;
8887

8988
assert!(registrar.collection_configs.len() == 2);
9089
assert!(max_voter_weight_record.max_voter_weight_expiry.is_none());
@@ -181,8 +180,7 @@ async fn test_update_max_voter_weight_record_provides_valid_expirey() -> Result<
181180
.await;
182181

183182
// Assert
184-
let max_voter_weight_total =
185-
(collection_1_weight * collection_1_size) + (collection_2_weight * collection_2_size);
183+
let max_voter_weight_total = collection_1_weight + collection_2_weight;
186184

187185
assert!(registrar.collection_configs.len() == 2);
188186
assert!(max_voter_weight_record.max_voter_weight == max_voter_weight_total as u64);

0 commit comments

Comments
 (0)