Skip to content

Commit

Permalink
sepeate exit cases
Browse files Browse the repository at this point in the history
  • Loading branch information
Ninjatosba committed Dec 4, 2024
1 parent 2c0c3d2 commit 05369f2
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 120 deletions.
268 changes: 149 additions & 119 deletions contracts/stream/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,153 +646,183 @@ pub fn execute_exit_stream(
info: MessageInfo,
salt: Option<Binary>,
) -> Result<Response, ContractError> {
// Load stream state and controller parameters
let mut stream_state = STREAM_STATE.load(deps.storage)?;
let controller_params = CONTROLLER_PARAMS.load(deps.storage)?;

// Load the position for the sender
let mut position = POSITIONS.load(deps.storage, &info.sender)?;

// Ensure the sender has not already exited
if position.exit_date != Timestamp::from_seconds(0) {
return Err(ContractError::SubscriberAlreadyExited {});
}

// Sync stream status and position
sync_stream_status(&mut stream_state, env.block.time);
sync_stream(&mut stream_state, env.block.time);

// Exit is handled differently based on the stream status
// If stream is ended and threshold is reached, we need to calculate the final position and send the funds
// This is the normal exit scenario
// If stream is ended and threshold is not reached, we need to refund the in tokens to the subscriber
// This is a full refund exit scenario
// If stream is cancelled, we need to refund the in tokens to the subscriber
// This is also a full refund exit scenario

// Match on stream status and threshold to determine exit behavior
match (
stream_state.status_info.clone().status,
stream_state.check_threshold(),
) {
// Normal exit scenario: stream is ended and threshold is reached
(Status::Ended, true) | (Status::Finalized(FinalizedStatus::ThresholdReached), _) => {
let mut position = POSITIONS.load(deps.storage, &info.sender)?;
sync_position(
stream_state.dist_index,
stream_state.shares,
stream_state.status_info.last_updated,
stream_state.in_supply,
return handle_normal_exit(

Check failure on line 672 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

unneeded `return` statement

Check failure on line 672 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

unneeded `return` statement
deps,
env,
info,
salt,
&mut stream_state,
&controller_params,
&mut position,
)?;

stream_state.shares = stream_state.shares.checked_sub(position.shares)?;
);
}

STREAM_STATE.save(deps.storage, &stream_state)?;
position.exit_date = env.block.time;
POSITIONS.save(deps.storage, &position.owner, &position)?;
// Full refund exit scenario: stream ended with threshold not reached or cancelled
(Status::Ended, false)
| (Status::Finalized(FinalizedStatus::ThresholdNotReached), _)
| (Status::Cancelled, _) => {
return handle_full_refund_exit(deps, env, info, &mut stream_state, &mut position);

Check failure on line 687 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

unneeded `return` statement

Check failure on line 687 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

unneeded `return` statement
}

let swap_fee = Decimal256::from_ratio(position.spent, Uint128::one())
.checked_mul(controller_params.exit_fee_percent)?
* Uint256::one();
// Error case: operation not allowed
_ => Err(ContractError::OperationNotAllowed {
current_status: stream_state.status_info.status.to_string(),
}),
}
}

let mut messages: Vec<CosmosMsg> = vec![];
let mut attributes: Vec<Attribute> = vec![];
/// Handles the normal exit scenario where the stream has ended and the threshold is reached
fn handle_normal_exit(
deps: DepsMut,
env: Env,
info: MessageInfo,
salt: Option<Binary>,
stream_state: &mut StreamState,
controller_params: &ControllerParams,
position: &mut Position,
) -> Result<Response, ContractError> {
sync_position(
stream_state.dist_index,
stream_state.shares,
stream_state.status_info.last_updated,
stream_state.in_supply,
position,
)?;

let uint128_purchased = Uint128::try_from(position.purchased)?;
// Update stream shares and position exit date
stream_state.shares = stream_state.shares.checked_sub(position.shares)?;
STREAM_STATE.save(deps.storage, &stream_state)?;

Check failure on line 717 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

this expression creates a reference which is immediately dereferenced by the compiler

Check failure on line 717 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

this expression creates a reference which is immediately dereferenced by the compiler
position.exit_date = env.block.time;
POSITIONS.save(deps.storage, &position.owner, &position)?;

Check failure on line 719 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

this expression creates a reference which is immediately dereferenced by the compiler

Check failure on line 719 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

this expression creates a reference which is immediately dereferenced by the compiler

let mut vesting_flag = false;
// Calculate exit fee
let swap_fee = Decimal256::from_ratio(position.spent, Uint128::one())
.checked_mul(controller_params.exit_fee_percent)?
* Uint256::one();

// Prepare messages and attributes for the response
let mut messages = vec![];
let mut attributes = vec![];

let uint128_purchased = Uint128::try_from(position.purchased)?;
let mut vesting_flag = false;

if let Some(post_stream_actions) = POST_STREAM.may_load(deps.storage)? {
if let Some(vesting_config) = post_stream_actions.subscriber_vesting {
let vesting_checksum = deps
.querier
.query_wasm_code_info(controller_params.vesting_code_id)?
.checksum;

let (vesting_msgs, vesting_attributes, vesting_addr) = vesting_operations(
&deps,
env.contract.address,
vesting_checksum,
info.sender.clone(),
salt,
stream_state.status_info.end_time,
controller_params.vesting_code_id,
uint128_purchased,
stream_state.out_asset.denom.clone(),
vesting_config,
)?;
messages.extend(vesting_msgs);
attributes.extend(vesting_attributes);
SUBSCRIBER_VESTING.save(deps.storage, info.sender.clone(), &vesting_addr)?;
vesting_flag = true;
}
}

let post_stream_actions = POST_STREAM.may_load(deps.storage)?;
if !vesting_flag {
let send_msg = CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: stream_state.out_asset.denom.to_string(),
amount: uint128_purchased,
}],
});
messages.push(send_msg);
}

if let Some(post_stream_actions) = post_stream_actions {
if let Some(vesting_config) = post_stream_actions.subscriber_vesting {
let vesting_checksum = deps
.querier
.query_wasm_code_info(controller_params.vesting_code_id)?
.checksum;
let (vesting_msgs, vesting_attributes, vesting_addr) = vesting_operations(
&deps,
env.contract.address,
vesting_checksum,
info.sender.clone(),
salt,
stream_state.status_info.end_time,
controller_params.vesting_code_id,
uint128_purchased,
stream_state.out_asset.denom.clone(),
vesting_config,
)?;
messages.extend(vesting_msgs);
attributes.extend(vesting_attributes);
SUBSCRIBER_VESTING.save(deps.storage, info.sender.clone(), &vesting_addr)?;
vesting_flag = true;
}
}
if !position.in_balance.is_zero() {
let unspent = position.in_balance;
let uint128_unspent = Uint128::try_from(unspent)?;
let unspent_msg = CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: stream_state.in_denom.clone(),
amount: uint128_unspent,
}],
});
messages.push(unspent_msg);
}

if !vesting_flag {
let send_msg = CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: stream_state.out_asset.denom.to_string(),
amount: uint128_purchased,
}],
});
messages.push(send_msg);
}
attributes.extend(vec![
attr("action", "exit_stream"),
attr("spent", position.spent.checked_sub(swap_fee)?),
attr("purchased", position.purchased),
attr("swap_fee_paid", swap_fee),
]);

if !position.in_balance.is_zero() {
let unspent = position.in_balance;
let uint128_unspent = Uint128::try_from(unspent)?;
let unspent_msg = CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: stream_state.in_denom,
amount: uint128_unspent,
}],
});
messages.push(unspent_msg);
}
Ok(Response::new()
.add_messages(messages)
.add_attributes(attributes))
}

attributes.extend(vec![
attr("action", "exit_stream"),
attr("spent", position.spent.checked_sub(swap_fee)?),
attr("purchased", position.purchased),
attr("swap_fee_paid", swap_fee),
]);
/// Handles the full refund exit scenario, where the stream is cancelled or threshold is not reached
fn handle_full_refund_exit(
deps: DepsMut,
env: Env,
info: MessageInfo,
stream_state: &mut StreamState,
position: &mut Position,
) -> Result<Response, ContractError> {
// Calculate total balance and update exit date
let total_balance = position.in_balance + position.spent;
position.exit_date = env.block.time;
position.last_updated = env.block.time;
POSITIONS.save(deps.storage, &position.owner, &position)?;

Check failure on line 807 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

this expression creates a reference which is immediately dereferenced by the compiler

Check failure on line 807 in contracts/stream/src/contract.rs

View workflow job for this annotation

GitHub Actions / Lints

this expression creates a reference which is immediately dereferenced by the compiler

Ok(Response::new()
.add_messages(messages)
.add_attributes(attributes))
}
let send_msg = build_u128_bank_send_msg(
stream_state.in_denom.clone(),
info.sender.to_string(),
total_balance,
)?;
let attributes = vec![
attr("action", "exit_cancelled"),
attr("to_address", info.sender.to_string()),
attr("total_balance", total_balance),
attr("exit_date", position.exit_date.to_string()),
attr("last_updated", position.last_updated.to_string()),
];

(Status::Ended, false)
| (Status::Finalized(FinalizedStatus::ThresholdNotReached), _)
| (Status::Cancelled, _) => {
// no need to sync position here, we just need to return total balance
let total_balance = position.in_balance + position.spent;
// sync position exit date
position.exit_date = env.block.time;
position.last_updated = env.block.time;
POSITIONS.save(deps.storage, &position.owner, &position)?;

let send_msg = build_u128_bank_send_msg(
stream_state.in_denom.clone(),
info.sender.to_string(),
total_balance,
)?;
let attributes = vec![
attr("action", "exit_cancelled"),
attr("to_address", info.sender.to_string()),
attr("total_balance", total_balance),
attr("exit_date", position.exit_date.to_string()),
attr("last_updated", position.last_updated.to_string()),
];
// send funds to the sender
let res = Response::new()
.add_message(send_msg)
.add_attributes(attributes);

Ok(res)
}
_ => {
return Err(ContractError::OperationNotAllowed {
current_status: stream_state.status_info.status.to_string(),
});
}
}
// Return the funds to the sender
Ok(Response::new()
.add_message(send_msg)
.add_attributes(attributes))
}

#[cfg_attr(not(feature = "library"), entry_point)]
Expand Down
8 changes: 7 additions & 1 deletion tests/src/tests/streamswap_tests/exit_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ mod exit_stream {
&[],
)
.unwrap();

let funds = get_funds_from_res(res);

assert_eq!(funds[0].1.amount, Uint128::from(100u128));
assert_eq!(funds[0].1.denom, "out_denom");
assert_eq!(funds.len(), 1);
}

#[test]
Expand Down Expand Up @@ -444,7 +450,7 @@ mod exit_stream {
// Attempting to exit without a position
let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None };

let err = app
let _err = app
.execute_contract(
test_accounts.subscriber_2.clone(), // No position for this subscriber
Addr::unchecked(stream_swap_contract_address.clone()),
Expand Down

0 comments on commit 05369f2

Please sign in to comment.