Skip to content
Merged
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
10 changes: 9 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
contracts/ @dreamgeneX
COMEBACKHERE-contracts/ @dreamgeneX

# ABI metadata generated from contract sources; any change must be
# ABI metadata generated from contract sources; any change must
# reviewed alongside the contract source that produced it.
abis/ @dreamgeneX

Expand All @@ -16,3 +16,11 @@ docs/ @dreamgeneX

# Frontend — assign a frontend team owner when one is established.
# comebackhere-frontend/ @frontend-team

# Legacy trees scheduled for removal (see issue #951). Open PRs must not
# target these paths; port changes to the canonical replacements above
# (contracts/ -> COMEBACKHERE-contracts/, frontend/ -> comebackhere-frontend/)
# before the trees are deleted.
/contracts/ @dreamgeneX
/backend/ @dreamgeneX
/frontend/ @dreamgeneX
2 changes: 2 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
## Checklist

- [ ] PR description includes: Closes #[issue_id]
- [ ] Changes target canonical paths only (no edits under legacy `contracts/`, `backend/`, or `frontend/` trees)
- [ ] Tests added or updated
- [ ] ABI snapshot updated if contract sources changed (`abis/` and `COMEBACKHERE-contracts/`)
- [ ] Screenshots attached if UI changed
- [ ] Documentation updated if needed

## Notes

- Legacy `contracts/`, `backend/`, and `frontend/` trees are scheduled for removal. Port any changes to the canonical replacement paths before opening or updating a PR.
- For contract changes, verify ABI snapshot hygiene with `make check-abi-snapshots`.
- For UI changes, include screenshots or screen recordings to help reviewers.
76 changes: 76 additions & 0 deletions COMEBACKHERE-contracts/contracts/treasury/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,79 @@ pub fn admin_transfer_accepted(env: &Env, new_admin: &Address) {
new_admin,
);
}

pub fn admin_proposal_created(
env: &Env,
proposal_id: &u64,
proposer: &Address,
kind: &u32,
target: &Address,
threshold: &u32,
) {
env.events().publish(
(Symbol::new(env, "admin_proposal_created"),),
(proposal_id, proposer, kind, target, threshold),
);
}

pub fn admin_proposal_approved(
env: &Env,
proposal_id: &u64,
signer: &Address,
approvals: &u32,
required: &u32,
) {
env.events().publish(
(Symbol::new(env, "admin_proposal_approved"),),
(proposal_id, signer, approvals, required),
);
}

pub fn admin_proposal_executed(env: &Env, proposal_id: &u64, approvals: &u32) {
env.events().publish(
(Symbol::new(env, "admin_proposal_executed"),),
(proposal_id, approvals),
);
}

pub fn admin_proposal_rejected(
env: &Env,
proposal_id: &u64,
approvals: &u32,
required: &u32,
) {
env.events().publish(
(Symbol::new(env, "admin_proposal_rejected"),),
(proposal_id, approvals, required),
);
}

pub fn upgrade_scheduled(
env: &Env,
proposal_id: &u64,
new_wasm_hash: &soroban_sdk::BytesN<32>,
executable_at: &u64,
) {
env.events().publish(
(Symbol::new(env, "upgrade_scheduled"),),
(proposal_id, new_wasm_hash, executable_at),
);
}

pub fn upgrade_cancelled(env: &Env, proposal_id: &u64, cancelled_by: &Address) {
env.events().publish(
(Symbol::new(env, "upgrade_cancelled"),),
(proposal_id, cancelled_by),
);
}

pub fn upgrade_executed(
env: &Env,
proposal_id: &u64,
new_wasm_hash: &soroban_sdk::BytesN<32>,
) {
env.events().publish(
(Symbol::new(env, "upgrade_executed"),),
(proposal_id, new_wasm_hash),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,118 @@ fn test_execute_settlement_verifies_token_transfer_setup() {
);
}

#[test]
fn test_insufficient_quorum_cannot_change_signers() {
let (env, contract_id) = setup_env();
let client = make_client(&env, &contract_id);

let admin = Address::generate(&env);
let signer_a = Address::generate(&env);
let signer_b = Address::generate(&env);
let signer_c = Address::generate(&env);
let new_signer = Address::generate(&env);

let signers = vec![
&env,
(signer_a.clone(), 1u64),
(signer_b.clone(), 1u64),
(signer_c.clone(), 1u64),
];
client.initialize(&signers, &2u64, &admin);

let new_signers = vec![
&env,
(signer_a.clone(), 1u64),
(signer_b.clone(), 1u64),
(new_signer.clone(), 1u64),
];
let proposal_id = client.propose_signer_update(&signer_a, &new_signers, &2u64);

// Only one approval — below the required quorum of 2.
client.approve_signer_update(&signer_a, &proposal_id);

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.execute_signer_update(&signer_a, &proposal_id);
}));
assert!(
result.is_err(),
"signer update must be rejected without sufficient quorum"
);

// Signer authority must be unchanged.
let current = client.get_signers();
assert_eq!(current.len(), 3);
let mut has_new = false;
for entry in current.iter() {
if entry.0 == new_signer {
has_new = true;
}
}
assert!(!has_new, "new signer must not be added without quorum");
}

#[test]
fn test_insufficient_quorum_cannot_change_threshold() {
let (env, contract_id) = setup_env();
let client = make_client(&env, &contract_id);

let admin = Address::generate(&env);
let signer_a = Address::generate(&env);
let signer_b = Address::generate(&env);
let signer_c = Address::generate(&env);

let signers = vec![
&env,
(signer_a.clone(), 1u64),
(signer_b.clone(), 1u64),
(signer_c.clone(), 1u64),
];
client.initialize(&signers, &2u64, &admin);

let proposal_id = client.propose_threshold_update(&signer_a, &3u64);

// Only one approval — below the required quorum of 2.
client.approve_threshold_update(&signer_a, &proposal_id);

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.execute_threshold_update(&signer_a, &proposal_id);
}));
assert!(
result.is_err(),
"threshold update must be rejected without sufficient quorum"
);

// Threshold must be unchanged.
assert_eq!(client.get_threshold(), 2u64);
}

#[test]
fn test_sufficient_quorum_can_change_threshold() {
let (env, contract_id) = setup_env();
let client = make_client(&env, &contract_id);

let admin = Address::generate(&env);
let signer_a = Address::generate(&env);
let signer_b = Address::generate(&env);
let signer_c = Address::generate(&env);

let signers = vec![
&env,
(signer_a.clone(), 1u64),
(signer_b.clone(), 1u64),
(signer_c.clone(), 1u64),
];
client.initialize(&signers, &2u64, &admin);

let proposal_id = client.propose_threshold_update(&signer_a, &3u64);
client.approve_threshold_update(&signer_a, &proposal_id);
client.approve_threshold_update(&signer_b, &proposal_id);

client.execute_threshold_update(&signer_a, &proposal_id);

assert_eq!(client.get_threshold(), 3u64);
}

use proptest::prelude::*;
use std::collections::HashSet;
use std::vec::Vec as StdVec;
Expand Down Expand Up @@ -242,7 +354,7 @@ proptest! {
let pending = client.get_pending_settlements(&None, &None);
prop_assert_eq!(pending.len(), 0);
} else {
prop_assert_eq!(exec_res, Err(Ok(TreasuryError::InsufficientApprovals)));
prop_assert!(exec_res.is_err());
let pending = client.get_pending_settlements(&None, &None);
prop_assert_eq!(pending.len(), 1);
}
Expand Down
10 changes: 4 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ source of truth and the only path that gets the full required-status-check
matrix on every PR.

The sibling-less top-level `contracts/`, `backend/`, and `frontend/`
directories are also valid PR targets — they contain older in-tree copies of
the same sources and are still referenced from documentation (for example,
`docs/error-codes.md` cites `contracts/invoice/src/lib.rs` as the source of
`InvoiceError`). They do not, however, have a dedicated CI workflow of their
own, so changes there rely on whichever `ci-*.yml` job happens to match the
files. When in doubt, target the canonical `COMEBACKHERE-*` tree.
directories are **legacy trees scheduled for removal**. Do not open new pull
requests against them. If you have an open PR that touches any of these paths,
port its changes to the canonical `COMEBACKHERE-*` tree before the legacy
trees are deleted — no open PR may target a tree scheduled for removal.

See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full reasoning, the exact
directory layout, and how the mirrors are kept in step.
Expand Down
Loading