@@ -39,15 +39,16 @@ use soroban_sdk::{contract, contractimpl, token, vec, Address, Env, InvokeError,
3939
4040use errors:: StreamError ;
4141use events:: {
42- AdminTransferredEvent , FeeCollectedEvent , FeeConfigUpdatedEvent , InitializedEvent ,
43- RecipientTransferredEvent , StreamCancelledEvent , StreamCompletedEvent , StreamCreatedEvent ,
44- StreamPausedEvent , StreamResumedEvent , StreamToppedUpEvent , TokensWithdrawnEvent ,
42+ AdminTransferredEvent , DisputeInitiatedEvent , DisputeResolvedEvent , FeeCollectedEvent ,
43+ FeeConfigUpdatedEvent , InitializedEvent , RecipientTransferredEvent , StreamCancelledEvent ,
44+ StreamCompletedEvent , StreamCreatedEvent , StreamPausedEvent , StreamResumedEvent ,
45+ StreamToppedUpEvent , TokensWithdrawnEvent ,
4546} ;
4647use storage:: {
4748 config_exists, load_config, load_stream, next_stream_id, save_config, save_stream,
4849 try_load_config, try_load_stream,
4950} ;
50- use types:: { BatchStreamInput , ProtocolConfig , Stream , StreamStatus } ;
51+ use types:: { BatchStreamInput , DisputeInitiatedData , DisputeState , ProtocolConfig , Stream , StreamStatus } ;
5152
5253/// Maximum allowed protocol fee: 1 000 bps = 10%.
5354const MAX_FEE_RATE_BPS : u32 = 1_000 ;
@@ -263,6 +264,8 @@ impl StreamContract {
263264 paused : false ,
264265 paused_at : None ,
265266 status : StreamStatus :: Active ,
267+ arbiter : None ,
268+ dispute_state : DisputeState :: None ,
266269 } ,
267270 ) ;
268271
@@ -356,6 +359,8 @@ impl StreamContract {
356359 paused : false ,
357360 paused_at : None ,
358361 status : StreamStatus :: Active ,
362+ arbiter : None ,
363+ dispute_state : DisputeState :: None ,
359364 } ;
360365 save_stream ( & env, stream_id, & stream) ;
361366 env. events ( ) . publish (
@@ -419,6 +424,8 @@ impl StreamContract {
419424 paused : false ,
420425 paused_at : None ,
421426 status : StreamStatus :: Active ,
427+ arbiter : None ,
428+ dispute_state : DisputeState :: None ,
422429 } ,
423430 ) ;
424431 env. events ( ) . publish (
@@ -785,6 +792,11 @@ impl StreamContract {
785792 Self :: validate_stream_ownership ( & stream, & sender) ?;
786793 Self :: validate_stream_active ( & stream) ?;
787794
795+ // Block unilateral cancellation if a dispute is in progress.
796+ if let DisputeState :: Initiated ( _) = & stream. dispute_state {
797+ return Err ( StreamError :: DisputeInProgress ) ;
798+ }
799+
788800 let now = env. ledger ( ) . timestamp ( ) ;
789801 let accrued_amount = Self :: calculate_claimable ( & stream, now) ;
790802
@@ -942,6 +954,240 @@ impl StreamContract {
942954 Ok ( new_end_time)
943955 }
944956
957+ // ─── Escrow & Dispute Resolution ──────────────────────────────────────────
958+
959+ /// Create a new escrow-enabled payment stream with an arbiter.
960+ ///
961+ /// Identical to `create_stream` but accepts an `arbiter` address that can
962+ /// mediate disputes. Either party can initiate a dispute, which freezes
963+ /// accrual and blocks unilateral cancellation until the arbiter resolves it.
964+ ///
965+ /// # Errors
966+ /// Same as `create_stream`.
967+ pub fn create_escrow_stream (
968+ env : Env ,
969+ sender : Address ,
970+ recipient : Address ,
971+ token_address : Address ,
972+ amount : i128 ,
973+ duration : u64 ,
974+ arbiter : Address ,
975+ ) -> Result < u64 , StreamError > {
976+ sender. require_auth ( ) ;
977+
978+ if amount <= 0 {
979+ return Err ( StreamError :: InvalidAmount ) ;
980+ }
981+ if duration == 0 {
982+ return Err ( StreamError :: InvalidDuration ) ;
983+ }
984+ Self :: validate_token_contract ( & env, & token_address) ?;
985+
986+ let stream_id = next_stream_id ( & env) ;
987+ let start_time = env. ledger ( ) . timestamp ( ) ;
988+
989+ let token_client = token:: Client :: new ( & env, & token_address) ;
990+ let contract_address = env. current_contract_address ( ) ;
991+ token_client. transfer ( & sender, & contract_address, & amount) ;
992+
993+ let net_amount = Self :: collect_fee ( & env, & token_address, amount, stream_id) ?;
994+ let rate_per_second = net_amount / ( duration as i128 ) ;
995+
996+ if rate_per_second == 0 {
997+ return Err ( StreamError :: InvalidRate ) ;
998+ }
999+
1000+ save_stream (
1001+ & env,
1002+ stream_id,
1003+ & Stream {
1004+ sender : sender. clone ( ) ,
1005+ recipient : recipient. clone ( ) ,
1006+ token_address : token_address. clone ( ) ,
1007+ rate_per_second,
1008+ deposited_amount : net_amount,
1009+ withdrawn_amount : 0 ,
1010+ start_time,
1011+ last_update_time : start_time,
1012+ cliff_time : None ,
1013+ is_active : true ,
1014+ paused : false ,
1015+ paused_at : None ,
1016+ status : StreamStatus :: Active ,
1017+ arbiter : Some ( arbiter) ,
1018+ dispute_state : DisputeState :: None ,
1019+ } ,
1020+ ) ;
1021+
1022+ env. events ( ) . publish (
1023+ ( Symbol :: new ( & env, "stream_created" ) , stream_id) ,
1024+ StreamCreatedEvent {
1025+ stream_id,
1026+ sender,
1027+ recipient,
1028+ rate_per_second,
1029+ token_address,
1030+ deposited_amount : net_amount,
1031+ start_time,
1032+ } ,
1033+ ) ;
1034+
1035+ Ok ( stream_id)
1036+ }
1037+
1038+ /// Initiate a dispute on an escrow-enabled stream.
1039+ ///
1040+ /// Either the sender or recipient can initiate. Once initiated, token
1041+ /// accrual is frozen (stream is paused) and unilateral cancellation or
1042+ /// withdrawal is blocked until the arbiter resolves the dispute.
1043+ ///
1044+ /// # Errors
1045+ /// - `StreamNotFound` — no stream exists with `stream_id`.
1046+ /// - `Unauthorized` — caller is neither sender nor recipient.
1047+ /// - `StreamNotActive` — stream is inactive.
1048+ /// - `NoArbiterConfigured` — stream has no arbiter.
1049+ /// - `DisputeAlreadyActive` — a dispute is already in progress.
1050+ pub fn initiate_dispute (
1051+ env : Env ,
1052+ caller : Address ,
1053+ stream_id : u64 ,
1054+ ) -> Result < ( ) , StreamError > {
1055+ caller. require_auth ( ) ;
1056+
1057+ let mut stream = load_stream ( & env, stream_id) ?;
1058+
1059+ // Only sender or recipient may initiate.
1060+ if stream. sender != caller && stream. recipient != caller {
1061+ return Err ( StreamError :: Unauthorized ) ;
1062+ }
1063+ if !stream. is_active {
1064+ return Err ( StreamError :: StreamNotActive ) ;
1065+ }
1066+ if stream. arbiter . is_none ( ) {
1067+ return Err ( StreamError :: NoArbiterConfigured ) ;
1068+ }
1069+ if let DisputeState :: Initiated ( _) = & stream. dispute_state {
1070+ return Err ( StreamError :: DisputeAlreadyActive ) ;
1071+ }
1072+
1073+ let now = env. ledger ( ) . timestamp ( ) ;
1074+
1075+ // Freeze accrual by pausing the stream at the dispute moment.
1076+ if !stream. paused {
1077+ stream. paused = true ;
1078+ stream. paused_at = Some ( now) ;
1079+ stream. status = StreamStatus :: Paused ;
1080+ }
1081+
1082+ stream. dispute_state = DisputeState :: Initiated ( DisputeInitiatedData {
1083+ initiator : caller. clone ( ) ,
1084+ timestamp : now,
1085+ } ) ;
1086+ save_stream ( & env, stream_id, & stream) ;
1087+
1088+ env. events ( ) . publish (
1089+ ( Symbol :: new ( & env, "dispute_initiated" ) , stream_id) ,
1090+ DisputeInitiatedEvent {
1091+ stream_id,
1092+ initiator : caller,
1093+ timestamp : now,
1094+ } ,
1095+ ) ;
1096+
1097+ Ok ( ( ) )
1098+ }
1099+
1100+ /// Resolve an active dispute as the designated arbiter.
1101+ ///
1102+ /// The arbiter splits the remaining deposit (after already-withdrawn
1103+ /// amounts) between sender and recipient. The split must be exact:
1104+ /// `sender_payout + recipient_payout == remaining_deposit`.
1105+ ///
1106+ /// Both payouts are transferred atomically and the stream is terminated.
1107+ ///
1108+ /// # Errors
1109+ /// - `StreamNotFound` — no stream exists with `stream_id`.
1110+ /// - `Unauthorized` — caller is not the stream's arbiter.
1111+ /// - `NoActiveDispute` — no dispute is in progress.
1112+ /// - `InvalidDisputeSplit` — payouts do not sum to remaining deposit.
1113+ pub fn resolve_dispute (
1114+ env : Env ,
1115+ arbiter : Address ,
1116+ stream_id : u64 ,
1117+ sender_payout : i128 ,
1118+ recipient_payout : i128 ,
1119+ ) -> Result < ( ) , StreamError > {
1120+ arbiter. require_auth ( ) ;
1121+
1122+ let mut stream = load_stream ( & env, stream_id) ?;
1123+
1124+ // Verify the caller is the stream's arbiter.
1125+ match & stream. arbiter {
1126+ Some ( a) if * a == arbiter => { }
1127+ _ => return Err ( StreamError :: Unauthorized ) ,
1128+ }
1129+
1130+ // Verify a dispute is active.
1131+ match & stream. dispute_state {
1132+ DisputeState :: Initiated ( _) => { }
1133+ _ => return Err ( StreamError :: NoActiveDispute ) ,
1134+ }
1135+
1136+ // The remaining deposit is everything not yet withdrawn.
1137+ let remaining = stream
1138+ . deposited_amount
1139+ . saturating_sub ( stream. withdrawn_amount ) ;
1140+
1141+ // Enforce exact split.
1142+ if sender_payout < 0 || recipient_payout < 0 {
1143+ return Err ( StreamError :: InvalidDisputeSplit ) ;
1144+ }
1145+ if sender_payout
1146+ . checked_add ( recipient_payout)
1147+ . ok_or ( StreamError :: ArithmeticOverflow ) ?
1148+ != remaining
1149+ {
1150+ return Err ( StreamError :: InvalidDisputeSplit ) ;
1151+ }
1152+
1153+ // Effects: terminate the stream.
1154+ stream. is_active = false ;
1155+ stream. status = StreamStatus :: Cancelled ;
1156+ stream. paused = false ;
1157+ stream. paused_at = Option :: None ;
1158+ stream. dispute_state = DisputeState :: Resolved ;
1159+ stream. withdrawn_amount = stream. deposited_amount ;
1160+
1161+ let sender_addr = stream. sender . clone ( ) ;
1162+ let recipient_addr = stream. recipient . clone ( ) ;
1163+
1164+ // Persist state before external calls (CEI).
1165+ save_stream ( & env, stream_id, & stream) ;
1166+
1167+ // Interactions: transfer payouts.
1168+ let token_client = token:: Client :: new ( & env, & stream. token_address ) ;
1169+ let contract_address = env. current_contract_address ( ) ;
1170+
1171+ if sender_payout > 0 {
1172+ token_client. transfer ( & contract_address, & sender_addr, & sender_payout) ;
1173+ }
1174+ if recipient_payout > 0 {
1175+ token_client. transfer ( & contract_address, & recipient_addr, & recipient_payout) ;
1176+ }
1177+
1178+ env. events ( ) . publish (
1179+ ( Symbol :: new ( & env, "dispute_resolved" ) , stream_id) ,
1180+ DisputeResolvedEvent {
1181+ stream_id,
1182+ arbiter,
1183+ sender_payout,
1184+ recipient_payout,
1185+ } ,
1186+ ) ;
1187+
1188+ Ok ( ( ) )
1189+ }
1190+
9451191 // ─── Read-only Queries ────────────────────────────────────────────────────
9461192
9471193 /// Returns the stream record for `stream_id`, or `None` if it does not exist.
0 commit comments