Skip to content

Commit d50266d

Browse files
committed
fix(subscription): require admin auth on init
- init() now calls admin.require_auth() before persisting any config, so only the address that will become the stored admin can perform initialization. Previously init stored the admin address with no signature verification at all. - Update AUTH_MATRIX.md and the subscription README to document that init requires admin authorization. - Add test_init_requires_admin_auth_without_persisting_state, mirroring the equivalent earnings contract test: an unauthorized init attempt is rejected and leaves the contract uninitialized, then a properly authorized init succeeds. - Add sub_init_invalid_non_admin_rejected to the AUTH_MATRIX.md compliance suite (auth_matrix.rs) and rename sub_init_valid_any_caller to sub_init_valid_admin_signs to reflect the new requirement. Closes #1376
1 parent f6b95ed commit d50266d

5 files changed

Lines changed: 50 additions & 3 deletions

File tree

contract/AUTH_MATRIX.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Contracts covered here (deployed by `contract/scripts/deploy.sh`):
5151

5252
| Method | Required signer(s) | Valid invocation example | Invalid invocation example |
5353
| --- | --- | --- | --- |
54-
| `init(env, admin, fee_bps, fee_recipient, token, price)` | `none` | Any caller initializes once with config values. | Re-initialization attempt after already initialized. |
54+
| `init(env, admin, fee_bps, fee_recipient, token, price)` | `admin` | `admin` signs and initializes once with config values. | Non-admin caller initializes without `admin` signature. |
5555
| `create_plan(env, creator, asset, amount, interval_days)` | `creator` | `creator` signs and creates a plan. | Non-creator caller submits plan for `creator`. |
5656
| `subscribe(env, fan, plan_id, _token)` | `fan` | `fan` signs and subscribes to `plan_id`. | Another address tries to subscribe using `fan` as parameter without `fan` auth. |
5757
| `is_subscriber(env, fan, creator)` | `none` | Any caller checks subscription status. | Expecting signer/auth to be required for read. |

contract/contracts/subscription/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ pub fn init(
6565

6666
One-time contract initialization. Stores admin, fee configuration, token address, and base subscription price.
6767

68+
**Requires `admin` authorization.**
69+
6870
**Panics** with `AlreadyInitialized` if called again, `InvalidFeeBps` if `fee_bps > 10_000`,
6971
`InvalidTokenAddress` if `token` is the Stellar null address, or `InvalidPrice` if `price <= 0`.
7072

contract/contracts/subscription/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ pub struct MyfansContract;
128128
impl MyfansContract {
129129
/// Initialize the subscription contract once.
130130
///
131+
/// Requires `admin` to authorize the call.
132+
///
131133
/// Validates:
132134
/// * `fee_bps` must be ≤ 10000 (100%).
133135
/// * `token` must be a valid non-null address.
@@ -143,6 +145,7 @@ impl MyfansContract {
143145
if env.storage().instance().has(&DataKey::Admin) {
144146
panic_with_error!(&env, Error::AlreadyInitialized);
145147
}
148+
admin.require_auth();
146149
require_valid_fee_recipient(&env, &fee_recipient);
147150
require_valid_fee_bps(&env, fee_bps);
148151
require_valid_token_address(&env, &token);

contract/contracts/subscription/src/test.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,32 @@ fn test_init_rejects_non_positive_price() {
115115
);
116116
}
117117

118+
/// init() requires the supplied admin to authorize the call; a rejected
119+
/// attempt leaves the contract uninitialized, allowing a later valid init.
120+
#[test]
121+
fn test_init_requires_admin_auth_without_persisting_state() {
122+
let env = Env::default();
123+
let admin = Address::generate(&env);
124+
let token_address = env.register_stellar_asset_contract_v2(admin.clone());
125+
let token_client = token::Client::new(&env, &token_address.address());
126+
let contract_id = env.register_contract(None, MyfansContract);
127+
let client = MyfansContractClient::new(&env, &contract_id);
128+
let fee_recipient = Address::generate(&env);
129+
130+
let empty: &[SorobanAuthorizationEntry] = &[];
131+
env.set_auths(empty);
132+
assert!(
133+
client
134+
.try_init(&admin, &500u32, &fee_recipient, &token_client.address, &1000i128)
135+
.is_err(),
136+
"init must require admin auth"
137+
);
138+
139+
env.mock_all_auths();
140+
client.init(&admin, &500u32, &fee_recipient, &token_client.address, &1000i128);
141+
assert_eq!(client.admin(), admin);
142+
}
143+
118144
#[test]
119145
fn test_init_succeeds_sets_admin_and_configuration() {
120146
let (env, client, admin, token, _token_admin) = setup_test();

contract/contracts/subscription/tests/auth_matrix.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,9 @@ fn sub_setup(
450450
(sub, token, admin, creator, fan)
451451
}
452452

453-
/// init – no auth required; any caller initializes once.
453+
/// init – admin signs and initializes once.
454454
#[test]
455-
fn sub_init_valid_any_caller() {
455+
fn sub_init_valid_admin_signs() {
456456
let env = base_env();
457457
let (token, admin) = setup_token(&env);
458458
let fee_recipient = Address::generate(&env);
@@ -462,6 +462,22 @@ fn sub_init_valid_any_caller() {
462462
assert_eq!(client.admin(), admin);
463463
}
464464

465+
/// init – non-admin (no auth) is rejected.
466+
#[test]
467+
fn sub_init_invalid_non_admin_rejected() {
468+
let env = base_env();
469+
let (token, admin) = setup_token(&env);
470+
let fee_recipient = Address::generate(&env);
471+
let id = env.register_contract(None, MyfansContract);
472+
let client = MyfansContractClient::new(&env, &id);
473+
env.set_auths(EMPTY_AUTHS);
474+
let result = client.try_init(&admin, &500u32, &fee_recipient, &token.address, &1000i128);
475+
assert!(
476+
result.is_err(),
477+
"non-admin must not initialize subscription contract"
478+
);
479+
}
480+
465481
/// init – re-initialization is rejected.
466482
#[test]
467483
fn sub_init_invalid_reinit_rejected() {

0 commit comments

Comments
 (0)