Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 2e69f37

Browse files
authored
Governance: Proposals signed off by owner (#2835)
* feat: allow proposal owner to sign off proposal directly * chore: add proposal by owner sign off tests * chore: add test for owner trying to sign off proposal with signatory
1 parent aefbebe commit 2e69f37

File tree

5 files changed

+289
-33
lines changed

5 files changed

+289
-33
lines changed

governance/program/src/error.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub enum GovernanceError {
3333

3434
/// Governing Token Owner or Delegate must sign transaction
3535
#[error("Governing Token Owner or Delegate must sign transaction")]
36-
GoverningTokenOwnerOrDelegateMustSign,
36+
GoverningTokenOwnerOrDelegateMustSign, // 505
3737

3838
/// All votes must be relinquished to withdraw governing tokens
3939
#[error("All votes must be relinquished to withdraw governing tokens")]
@@ -53,47 +53,47 @@ pub enum GovernanceError {
5353

5454
/// Invalid Proposal for ProposalTransaction,
5555
#[error("Invalid Proposal for ProposalTransaction,")]
56-
InvalidProposalForProposalTransaction,
56+
InvalidProposalForProposalTransaction, // 510
5757

5858
/// Invalid Signatory account address
5959
#[error("Invalid Signatory account address")]
60-
InvalidSignatoryAddress,
60+
InvalidSignatoryAddress, // 511
6161

6262
/// Signatory already signed off
6363
#[error("Signatory already signed off")]
64-
SignatoryAlreadySignedOff,
64+
SignatoryAlreadySignedOff, // 512
6565

6666
/// Signatory must sign
6767
#[error("Signatory must sign")]
68-
SignatoryMustSign,
68+
SignatoryMustSign, // 513
6969

7070
/// Invalid Proposal Owner
7171
#[error("Invalid Proposal Owner")]
72-
InvalidProposalOwnerAccount,
72+
InvalidProposalOwnerAccount, // 514
7373

7474
/// Invalid Proposal for VoterRecord
7575
#[error("Invalid Proposal for VoterRecord")]
76-
InvalidProposalForVoterRecord,
76+
InvalidProposalForVoterRecord, // 515
7777

7878
/// Invalid GoverningTokenOwner for VoteRecord
7979
#[error("Invalid GoverningTokenOwner for VoteRecord")]
80-
InvalidGoverningTokenOwnerForVoteRecord,
80+
InvalidGoverningTokenOwnerForVoteRecord, // 516
8181

8282
/// Invalid Governance config: Vote threshold percentage out of range"
8383
#[error("Invalid Governance config: Vote threshold percentage out of range")]
84-
InvalidVoteThresholdPercentage,
84+
InvalidVoteThresholdPercentage, // 517
8585

8686
/// Proposal for the given Governance, Governing Token Mint and index already exists
8787
#[error("Proposal for the given Governance, Governing Token Mint and index already exists")]
88-
ProposalAlreadyExists,
88+
ProposalAlreadyExists, // 518
8989

9090
/// Token Owner already voted on the Proposal
9191
#[error("Token Owner already voted on the Proposal")]
92-
VoteAlreadyExists,
92+
VoteAlreadyExists, // 519
9393

9494
/// Owner doesn't have enough governing tokens to create Proposal
9595
#[error("Owner doesn't have enough governing tokens to create Proposal")]
96-
NotEnoughTokensToCreateProposal,
96+
NotEnoughTokensToCreateProposal, // 520
9797

9898
/// Invalid State: Can't edit Signatories
9999
#[error("Invalid State: Can't edit Signatories")]
@@ -132,7 +132,7 @@ pub enum GovernanceError {
132132

133133
/// Invalid State: Can't sign off
134134
#[error("Invalid State: Can't sign off")]
135-
InvalidStateCannotSignOff,
135+
InvalidStateCannotSignOff, // 530
136136

137137
/// Invalid State: Can't vote
138138
#[error("Invalid State: Can't vote")]

governance/program/src/instruction.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ pub enum GovernanceInstruction {
132132
/// 2. `[]` Program governed by this Governance account
133133
/// 3. `[writable]` Program Data account of the Program governed by this Governance account
134134
/// 4. `[signer]` Current Upgrade Authority account of the Program governed by this Governance account
135-
/// 5. `[]` Governing TokenOwnerRecord account (Used only if not signed by RealmAuthority)
135+
/// 5. `[]` Governing TokenOwnerRecord account (Used only if not signed by RealmAuthority)
136136
/// 6. `[signer]` Payer
137137
/// 7. `[]` bpf_upgradeable_loader program
138138
/// 8. `[]` System program
@@ -265,12 +265,17 @@ pub enum GovernanceInstruction {
265265
CancelProposal,
266266

267267
/// Signs off Proposal indicating the Signatory approves the Proposal
268-
/// When the last Signatory signs the Proposal state moves to Voting state
268+
/// When the last Signatory signs off the Proposal it enters Voting state
269+
/// Note: Adding signatories to a Proposal is a quality and not a security gate and
270+
/// it's entirely at the discretion of the Proposal owner
271+
/// If Proposal owner doesn't designate any signatories then can sign off the Proposal themself
269272
///
270273
/// 0. `[writable]` Proposal account
271274
/// 1. `[writable]` Signatory Record account
272-
/// 2. `[signer]` Signatory account
275+
/// 2. `[signer]` Signatory account signing off the Proposal
276+
/// Or Proposal owner if the owner hasn't appointed any signatories
273277
/// 3. `[]` Clock sysvar
278+
/// 4. `[]` Optional TokenOwnerRecord of the Proposal owner when self signing off the Proposal
274279
SignOffProposal,
275280

276281
/// Uses your voter weight (deposited Community or Council tokens) to cast a vote on a Proposal
@@ -982,16 +987,21 @@ pub fn sign_off_proposal(
982987
// Accounts
983988
proposal: &Pubkey,
984989
signatory: &Pubkey,
990+
proposal_owner_record: Option<&Pubkey>,
985991
) -> Instruction {
986992
let signatory_record_address = get_signatory_record_address(program_id, proposal, signatory);
987993

988-
let accounts = vec![
994+
let mut accounts = vec![
989995
AccountMeta::new(*proposal, false),
990996
AccountMeta::new(signatory_record_address, false),
991997
AccountMeta::new_readonly(*signatory, true),
992998
AccountMeta::new_readonly(sysvar::clock::id(), false),
993999
];
9941000

1001+
if let Some(proposal_owner_record) = proposal_owner_record {
1002+
accounts.push(AccountMeta::new_readonly(*proposal_owner_record, false))
1003+
}
1004+
9951005
let instruction = GovernanceInstruction::SignOffProposal;
9961006

9971007
Instruction {

governance/program/src/processor/process_sign_off_proposal.rs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use solana_program::{
1212
use crate::state::{
1313
enums::ProposalState, proposal::get_proposal_data,
1414
signatory_record::get_signatory_record_data_for_seeds,
15+
token_owner_record::get_token_owner_record_data_for_proposal_owner,
1516
};
1617

1718
/// Processes SignOffProposal instruction
@@ -29,26 +30,43 @@ pub fn process_sign_off_proposal(program_id: &Pubkey, accounts: &[AccountInfo])
2930
let mut proposal_data = get_proposal_data(program_id, proposal_info)?;
3031
proposal_data.assert_can_sign_off()?;
3132

32-
let mut signatory_record_data = get_signatory_record_data_for_seeds(
33-
program_id,
34-
signatory_record_info,
35-
proposal_info.key,
36-
signatory_info.key,
37-
)?;
38-
signatory_record_data.assert_can_sign_off(signatory_info)?;
33+
// If the owner of the proposal hasn't appointed any signatories then can sign off the proposal themself
34+
if proposal_data.signatories_count == 0 {
35+
let proposal_owner_record_info = next_account_info(account_info_iter)?; // 4
3936

40-
signatory_record_data.signed_off = true;
41-
signatory_record_data.serialize(&mut *signatory_record_info.data.borrow_mut())?;
37+
let proposal_owner_record_data = get_token_owner_record_data_for_proposal_owner(
38+
program_id,
39+
proposal_owner_record_info,
40+
&proposal_data.token_owner_record,
41+
)?;
42+
43+
// Proposal owner (TokenOwner) or its governance_delegate must be the signatory and sign this transaction
44+
proposal_owner_record_data.assert_token_owner_or_delegate_is_signer(signatory_info)?;
4245

43-
if proposal_data.signatories_signed_off_count == 0 {
4446
proposal_data.signing_off_at = Some(clock.unix_timestamp);
45-
proposal_data.state = ProposalState::SigningOff;
46-
}
47+
} else {
48+
let mut signatory_record_data = get_signatory_record_data_for_seeds(
49+
program_id,
50+
signatory_record_info,
51+
proposal_info.key,
52+
signatory_info.key,
53+
)?;
54+
55+
signatory_record_data.assert_can_sign_off(signatory_info)?;
4756

48-
proposal_data.signatories_signed_off_count = proposal_data
49-
.signatories_signed_off_count
50-
.checked_add(1)
51-
.unwrap();
57+
signatory_record_data.signed_off = true;
58+
signatory_record_data.serialize(&mut *signatory_record_info.data.borrow_mut())?;
59+
60+
if proposal_data.signatories_signed_off_count == 0 {
61+
proposal_data.signing_off_at = Some(clock.unix_timestamp);
62+
proposal_data.state = ProposalState::SigningOff;
63+
}
64+
65+
proposal_data.signatories_signed_off_count = proposal_data
66+
.signatories_signed_off_count
67+
.checked_add(1)
68+
.unwrap();
69+
}
5270

5371
// If all Signatories signed off we can start voting
5472
if proposal_data.signatories_signed_off_count == proposal_data.signatories_count {

governance/program/tests/process_sign_off_proposal.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use solana_program_test::tokio;
66

77
use program_test::*;
88
use spl_governance::{error::GovernanceError, state::enums::ProposalState};
9+
use spl_governance_tools::error::GovernanceToolsError;
910

1011
#[tokio::test]
1112
async fn test_sign_off_proposal() {
@@ -113,3 +114,188 @@ async fn test_sign_off_proposal_with_signatory_must_sign_error() {
113114
// Assert
114115
assert_eq!(err, GovernanceError::SignatoryMustSign.into());
115116
}
117+
118+
#[tokio::test]
119+
async fn test_sign_off_proposal_by_owner() {
120+
// Arrange
121+
let mut governance_test = GovernanceProgramTest::start_new().await;
122+
123+
let realm_cookie = governance_test.with_realm().await;
124+
let governed_account_cookie = governance_test.with_governed_account().await;
125+
126+
let token_owner_record_cookie = governance_test
127+
.with_community_token_deposit(&realm_cookie)
128+
.await
129+
.unwrap();
130+
131+
let mut account_governance_cookie = governance_test
132+
.with_account_governance(
133+
&realm_cookie,
134+
&governed_account_cookie,
135+
&token_owner_record_cookie,
136+
)
137+
.await
138+
.unwrap();
139+
140+
let proposal_cookie = governance_test
141+
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
142+
.await
143+
.unwrap();
144+
145+
let clock = governance_test.bench.get_clock().await;
146+
147+
// Act
148+
governance_test
149+
.sign_off_proposal_by_owner(&proposal_cookie, &token_owner_record_cookie)
150+
.await
151+
.unwrap();
152+
153+
// Assert
154+
let proposal_account = governance_test
155+
.get_proposal_account(&proposal_cookie.address)
156+
.await;
157+
158+
assert_eq!(0, proposal_account.signatories_count);
159+
assert_eq!(0, proposal_account.signatories_signed_off_count);
160+
assert_eq!(ProposalState::Voting, proposal_account.state);
161+
assert_eq!(Some(clock.unix_timestamp), proposal_account.signing_off_at);
162+
assert_eq!(Some(clock.unix_timestamp), proposal_account.voting_at);
163+
assert_eq!(Some(clock.slot), proposal_account.voting_at_slot);
164+
}
165+
166+
#[tokio::test]
167+
async fn test_sign_off_proposal_by_owner_with_owner_must_sign_error() {
168+
// Arrange
169+
let mut governance_test = GovernanceProgramTest::start_new().await;
170+
171+
let realm_cookie = governance_test.with_realm().await;
172+
let governed_account_cookie = governance_test.with_governed_account().await;
173+
174+
let token_owner_record_cookie = governance_test
175+
.with_community_token_deposit(&realm_cookie)
176+
.await
177+
.unwrap();
178+
179+
let mut account_governance_cookie = governance_test
180+
.with_account_governance(
181+
&realm_cookie,
182+
&governed_account_cookie,
183+
&token_owner_record_cookie,
184+
)
185+
.await
186+
.unwrap();
187+
188+
let proposal_cookie = governance_test
189+
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
190+
.await
191+
.unwrap();
192+
193+
// Act
194+
195+
let err = governance_test
196+
.sign_off_proposal_by_owner_using_instruction(
197+
&proposal_cookie,
198+
&token_owner_record_cookie,
199+
|i| i.accounts[2].is_signer = false, // signatory
200+
Some(&[]),
201+
)
202+
.await
203+
.err()
204+
.unwrap();
205+
206+
// Assert
207+
assert_eq!(
208+
err,
209+
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
210+
);
211+
}
212+
213+
#[tokio::test]
214+
async fn test_sign_off_proposal_by_owner_with_other_proposal_owner_error() {
215+
// Arrange
216+
let mut governance_test = GovernanceProgramTest::start_new().await;
217+
218+
let realm_cookie = governance_test.with_realm().await;
219+
let governed_account_cookie = governance_test.with_governed_account().await;
220+
221+
let token_owner_record_cookie = governance_test
222+
.with_community_token_deposit(&realm_cookie)
223+
.await
224+
.unwrap();
225+
226+
let mut account_governance_cookie = governance_test
227+
.with_account_governance(
228+
&realm_cookie,
229+
&governed_account_cookie,
230+
&token_owner_record_cookie,
231+
)
232+
.await
233+
.unwrap();
234+
235+
let proposal_cookie = governance_test
236+
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
237+
.await
238+
.unwrap();
239+
240+
let token_owner_record_cookie2 = governance_test
241+
.with_community_token_deposit(&realm_cookie)
242+
.await
243+
.unwrap();
244+
245+
// Act
246+
247+
let err = governance_test
248+
.sign_off_proposal_by_owner(&proposal_cookie, &token_owner_record_cookie2)
249+
.await
250+
.err()
251+
.unwrap();
252+
253+
// Assert
254+
assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into());
255+
}
256+
257+
#[tokio::test]
258+
async fn test_sign_off_proposal_by_owner_with_existing_signatories_error() {
259+
// Arrange
260+
let mut governance_test = GovernanceProgramTest::start_new().await;
261+
262+
let realm_cookie = governance_test.with_realm().await;
263+
let governed_account_cookie = governance_test.with_governed_account().await;
264+
265+
let token_owner_record_cookie = governance_test
266+
.with_community_token_deposit(&realm_cookie)
267+
.await
268+
.unwrap();
269+
270+
let mut account_governance_cookie = governance_test
271+
.with_account_governance(
272+
&realm_cookie,
273+
&governed_account_cookie,
274+
&token_owner_record_cookie,
275+
)
276+
.await
277+
.unwrap();
278+
279+
let proposal_cookie = governance_test
280+
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
281+
.await
282+
.unwrap();
283+
284+
governance_test
285+
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
286+
.await
287+
.unwrap();
288+
289+
// Act
290+
291+
let err = governance_test
292+
.sign_off_proposal_by_owner(&proposal_cookie, &token_owner_record_cookie)
293+
.await
294+
.err()
295+
.unwrap();
296+
297+
// Assert
298+
299+
// The instruction fails with AccountDoesNotExist because SignatoryRecord doesn't exist for owner
300+
assert_eq!(err, GovernanceToolsError::AccountDoesNotExist.into());
301+
}

0 commit comments

Comments
 (0)