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
9 changes: 9 additions & 0 deletions contracts/tip-splitter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,15 @@ impl TipSplitter {
/// deployment is usable through `tip_in_token` immediately and
/// `get_token`/`is_token_allowed` can never disagree about it.
pub fn __constructor(env: Env, admin: Address, token: Address) {
// The admin manages privileged operations (fees, pause, hooks, allowlist)
// while the token address represents the Stellar Asset Contract that all tips
// settle through. Providing the same address for both is a deployment error.
// The token address is recorded permanently and can never be changed post-deploy,
// so catching this misconfiguration at construction prevents deploying a permanently
// bricked contract requiring complete redeployment and migration of jars.
if admin == token {
panic!("admin and token cannot be the same address");
}
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Token, &token);
env.storage()
Expand Down
83 changes: 83 additions & 0 deletions contracts/tip-splitter/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8790,3 +8790,86 @@ fn create_recurring_rejects_invalid_message() {
let res = client.try_create_recurring(&supporter, &jar_id, &500, &100, &with_nul);
assert_eq!(res, Err(Ok(Error::InvalidMessage.into())));
}

/// The constructor must reject an admin and token that share the same address,
/// preventing deployment of a permanently misconfigured contract (#69).
#[test]
#[should_panic(expected = "admin and token cannot be the same address")]
fn constructor_rejects_admin_and_token_with_same_address() {
let env = Env::default();
let addr = Address::generate(&env);
env.register(TipSplitter, (addr.clone(), addr));
}

/// An amount large enough that `amount * bps` overflows i128 in the minimum-share
/// guard or distribution loop must fail with `Error::AmountOverflow` rather than
/// trapping the WASM runtime (#68).
#[test]
fn compute_shares_overflow_fails_with_amount_overflow() {
let s = setup();
let env = &s.env;
let client = TipSplitterClient::new(env, &s.contract);

let owner = Address::generate(env);
let alice = Address::generate(env);
let jar_id = String::from_str(env, "@overflow");
client.create_jar(
&owner,
&jar_id,
&vec![
env,
Split {
to: alice.clone(),
bps: 10_000,
},
],
);

let res = client.try_preview_split(&jar_id, &i128::MAX);
assert_eq!(res, Err(Ok(Error::AmountOverflow.into())));
}

/// A tipper who is also a recipient holding a non-final share keeps their own
/// share instead of funding the last recipient (#66).
/// In a jar with tipper (3000 bps) and another recipient (7000 bps), tipping 100:
/// only 70 leaves the tipper and the recipient receives 70, leaving 30 with the tipper.
#[test]
fn tipper_who_is_non_final_recipient_keeps_own_share() {
let s = setup();
let env = &s.env;
let client = TipSplitterClient::new(env, &s.contract);
let token = token::Client::new(env, &s.token);
let token_admin = token::StellarAssetClient::new(env, &s.token);

let owner = Address::generate(env);
let tipper = Address::generate(env);
let recipient = Address::generate(env);

token_admin.mint(&tipper, &100);

let jar_id = String::from_str(env, "@self-nonfinal");
client.create_jar(
&owner,
&jar_id,
&vec![
env,
Split {
to: tipper.clone(),
bps: 3000,
},
Split {
to: recipient.clone(),
bps: 7000,
},
],
);

client.tip(&tipper, &jar_id, &100, &String::from_str(env, "keep self share"));

// Tipper started with 100, kept 30, only 70 left:
assert_eq!(token.balance(&tipper), 30);
// Recipient received exactly 70:
assert_eq!(token.balance(&recipient), 70);
}


12 changes: 12 additions & 0 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ withdrawal.
match the on-chain event back to whatever it created off-chain. Never stored
and never checked by the contract.

## Limits

Quick reference for bounds enforced by the contract:

| Limit | Value | Constant | Error when exceeded |
|---|---|---|---|
| Basis point denominator | `10_000` | `BPS_DENOM` | `SplitOutOfRange` (11), `SplitSumNot100Pct` (12) |
| Maximum recipient count | `20` | `MAX_RECIPIENTS` | `TooManyRecipients` (6) |
| Maximum message length | `280` bytes | `MAX_MESSAGE_LEN` | `MessageTooLong` (8) |
| Maximum batch tips | `10` | `MAX_BATCH_TIPS` | `BatchTooLarge` (38) |
| Maximum jar id length | `33` bytes (`@` + 32) | `MAX_JAR_SLUG_LEN + 1` | `InvalidJarId` (9) |

## Types

```rust
Expand Down
Loading