Skip to content

Commit d51de36

Browse files
Merge pull request #346 from iheomadev/feature/299-300-301-302-docs-events-security-guardian-topup-test
docs: add Contract Events table, security review version, and top_up new-token test
2 parents c1913ab + 50ae84c commit d51de36

4 files changed

Lines changed: 156 additions & 0 deletions

File tree

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,43 @@ use will::{MAX_BENEFICIARIES, MAX_GUARDIANS, GUARDIAN_THRESHOLD};
135135

136136
`checkin_period_days` and `grace_period_days` passed to `create_will` must each be at least `1` day (and at most `MAX_PERIOD_DAYS`); a value of `0` panics with `WillError::InvalidPeriod`.
137137

138+
## Contract Events
139+
140+
Every state-mutating entry point publishes exactly one event so that off-chain indexers and SDK consumers can reconstruct will history without re-simulating transactions. The topic is a tuple of `(symbol, will_id)` unless noted otherwise. Payload fields are listed in order.
141+
142+
| Entry point | Topic symbol | Payload |
143+
|---|---|---|
144+
| `create_will` | `"created"` | `(owner: Address, token_count: u32, beneficiaries: Vec<Beneficiary>, checkin_deadline: u64)` |
145+
| `confirm_will` | `"confirmed"` | `owner: Address` |
146+
| `check_in` | `"checkin"` | `(owner: Address, next_deadline: u64)` |
147+
| `trigger_will` | `"triggered"` | `grace_period_ends: u64` |
148+
| `emergency_checkin` | `"emerg"` | `(owner: Address, next_deadline: u64)` |
149+
| `release_inheritance` | `"released"` | `(token_count: u32, beneficiaries_count: u32)` |
150+
| `cancel_will` | `"cancelled"` | `(owner: Address, token_count: u32)` |
151+
| `update_beneficiaries` | `"benefup"` | `(owner: Address, beneficiary_count: u32, beneficiaries: Vec<Beneficiary>)` |
152+
| `update_guardians` | `"guardup"` | `(owner: Address, guardians: Vec<Guardian>)` |
153+
| `update_will_settings` | `"setupd"` | `(owner: Address, update_fields: Vec<Symbol>)` — also emits `"guardup"` when guardians change |
154+
| `close_will` | `"closed"` | `owner: Address` |
155+
| `top_up` | `"topup"` | `(owner: Address, token: Address, amount: i128, new_balance: i128)` |
156+
| `guardian_trigger` | `"gvote"` | `(guardian: Address, weight: u32, total_weight: u32)` |
157+
| `guardian_cancel` (cancel vote) | `"gcvote"` | `(guardian: Address, weight: u32, total_weight: u32)` |
158+
| `guardian_cancel` (quorum reached) | `"gcancel"` | `(guardian: Address, next_deadline: u64)` |
159+
| `merge_wills` | `"merged"` | `(owner: Address, consumed_will_id: u64, new_balance: i128, beneficiaries: Vec<Beneficiary>)` — topic uses surviving will id |
160+
| `migrate_will` | `"migrated"` | `(owner: Address, from_version: u32, to_version: u32)` |
161+
| `clone_will` | `"cloned"` | `(source_id: u64, owner: Address)` — topic uses new will id |
162+
| `batch_create_wills` | `"batch"` | `will_ids: Vec<u64>` — topic is `(symbol, owner)` instead of `(symbol, will_id)` |
163+
| `archive_will` | `"archived"` | `owner: Address` |
164+
| `update_will_settings` (periods) | `"periodu"` | `(owner: Address, new_checkin_period_days: u64, new_grace_period_days: u64, next_deadline: u64)` |
165+
| `renounce_inheritance` | `"renounce"` | `(beneficiary: Address, owner: Address, beneficiaries: Vec<Beneficiary>)` |
166+
| `keeper_bounty` | `"bounty"` | `(keeper: Address, amount: i128)` |
167+
| `split_will` | `"split"` | `(new_id: u64, owner: Address, split_amount: i128)` — topic uses original will id |
168+
| `reveal_and_claim` | `"hclaim"` | `(claimant: Address, amount: i128)` |
169+
| `set_delegate` | `"delegset"` | `(owner: Address, delegate: Address)` |
170+
| `clear_delegate` | `"delegclr"` | `owner: Address` |
171+
| `batch_checkin` | `"batchchk"` | `(will_ids: Vec<u64>, count: u32)` — topic is `(symbol, owner)` instead of `(symbol, will_id)` |
172+
173+
The canonical source of truth for each event's exact topic and payload is [`contracts/will/src/events.rs`](./contracts/will/src/events.rs).
174+
138175
### Reading wills and Soroban's archival model (issue #166)
139176

140177
`get_will` and `get_wills_by_owner` / `get_wills_by_beneficiary` read a will's
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#![cfg(test)]
2+
3+
//! Regression test for issue #299: `top_up` supports adding a token that was
4+
//! not locked at `create_will` time (new-token path via `will.balances.get(token).unwrap_or(0)`).
5+
//!
6+
//! Acceptance criteria (from the issue):
7+
//! 1. A will is created with a single token (token_a).
8+
//! 2. `top_up` is called with a second, distinct token (token_b) never seen by
9+
//! that will before.
10+
//! 3. `get_will` reflects both token balances.
11+
//! 4. A subsequent `release_inheritance` correctly distributes both tokens to
12+
//! the beneficiary.
13+
14+
use soroban_sdk::{
15+
testutils::{Address as _, Ledger},
16+
token::{Client as TokenClient, StellarAssetClient},
17+
vec, Address, Env,
18+
};
19+
20+
use crate::{Allocation, Beneficiary, WillContract, WillContractClient, WillStatus};
21+
22+
const DAY: u64 = 86_400;
23+
24+
#[test]
25+
fn top_up_with_new_token_is_reflected_in_get_will_and_released() {
26+
let env = Env::default();
27+
env.mock_all_auths();
28+
env.ledger().set_timestamp(1_700_000_000);
29+
30+
let owner = Address::generate(&env);
31+
let beneficiary = Address::generate(&env);
32+
33+
// Set up token_a (the original will token).
34+
let sac_a = env.register_stellar_asset_contract_v2(owner.clone());
35+
let token_a_address = sac_a.address();
36+
StellarAssetClient::new(&env, &token_a_address).mint(&owner, &1_000_000);
37+
let token_a = TokenClient::new(&env, &token_a_address);
38+
39+
// Set up token_b (a brand-new token, not present at create_will).
40+
let sac_b = env.register_stellar_asset_contract_v2(owner.clone());
41+
let token_b_address = sac_b.address();
42+
StellarAssetClient::new(&env, &token_b_address).mint(&owner, &500_000);
43+
let token_b = TokenClient::new(&env, &token_b_address);
44+
45+
let contract_id = env.register(WillContract, ());
46+
let client = WillContractClient::new(&env, &contract_id);
47+
48+
// Step 1: create a will locked only with token_a.
49+
let will_id = client.create_will(
50+
&owner,
51+
&vec![&env, (token_a_address.clone(), 1_000_000_i128)],
52+
&vec![
53+
&env,
54+
Beneficiary {
55+
address: beneficiary.clone(),
56+
allocation: Allocation::Percentage(10_000),
57+
},
58+
],
59+
&90,
60+
&7,
61+
&vec![&env],
62+
&1,
63+
&None,
64+
&0,
65+
);
66+
67+
// Step 2: top up with token_b (never seen by this will before).
68+
client.top_up(&will_id, &owner, &token_b_address, &500_000);
69+
70+
// Step 3: get_will must reflect both token balances.
71+
let will = client.get_will(&will_id);
72+
assert_eq!(will.status, WillStatus::Active);
73+
assert_eq!(
74+
will.balances.get(token_a_address.clone()).unwrap(),
75+
1_000_000_i128,
76+
"token_a balance must remain 1_000_000 after top_up with token_b"
77+
);
78+
assert_eq!(
79+
will.balances.get(token_b_address.clone()).unwrap(),
80+
500_000_i128,
81+
"token_b balance must be 500_000 after top_up"
82+
);
83+
84+
// Step 4: release_inheritance must distribute both tokens to the beneficiary.
85+
env.ledger().with_mut(|l| l.timestamp += 91 * DAY);
86+
client.trigger_will(&will_id);
87+
env.ledger().with_mut(|l| l.timestamp += 8 * DAY);
88+
client.release_inheritance(&will_id, &None);
89+
90+
assert_eq!(
91+
token_a.balance(&beneficiary),
92+
1_000_000,
93+
"beneficiary must receive the full token_a balance"
94+
);
95+
assert_eq!(
96+
token_b.balance(&beneficiary),
97+
500_000,
98+
"beneficiary must receive the full token_b balance"
99+
);
100+
// Contract must hold no residual balance of either token.
101+
assert_eq!(token_a.balance(&client.address), 0);
102+
assert_eq!(token_b.balance(&client.address), 0);
103+
104+
let released_will = client.get_will(&will_id);
105+
assert_eq!(released_will.status, WillStatus::Released);
106+
}

contracts/will/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ mod merge_fixed_amount_test;
149149
#[cfg(test)]
150150
mod update_guardians_threshold_test;
151151

152+
/// Regression test for issue #299: `top_up` supports adding a token that was
153+
/// not locked at `create_will` time (new-token path).
154+
#[cfg(test)]
155+
mod issue_299_test;
156+
152157

153158
use soroban_sdk::{
154159
contract, contractimpl, panic_with_error, symbol_short, token, Address, Bytes, Env, Map, Vec,

docs/SECURITY-REVIEW.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
# Security Review Report — SoroWill Contract
22

33
**Date:** 2026-07-27
4+
**Contract version:** 1.0.0 (`CONTRACT_VERSION = 1_000_000`, git ref: `main` as of 2026-07-27)
45
**Scope:** `contracts/will/src/` — full contract source
56
**Reviewer:** Automated review pass
67
**Methodology:** Manual code review covering reentrancy, authorization bypass, integer overflow/underflow, and general Soroban security best practices.
78

9+
> **Staleness note:** This review was performed against contract version **1.0.0**. After any
10+
> change that alters observable contract behaviour (new entry points, changed authorization
11+
> paths, modified payout logic, or bumped `CONTRACT_VERSION`), a follow-up review should be
12+
> scheduled. To request a re-review, open an issue tagged `security-review` describing the
13+
> changes since this document's last update date, or contact the maintainers via
14+
> [`SECURITY.md`](../SECURITY.md).
15+
816
---
917

1018
## 1. Reentrancy Risk

0 commit comments

Comments
 (0)