Skip to content

Commit 297b7d0

Browse files
authored
Merge pull request #135 from Lay3rLabs/release_stake/splitter
Implement release_stake for splitter
2 parents b34fccd + f71bfd7 commit 297b7d0

4 files changed

Lines changed: 265 additions & 8 deletions

File tree

contracts/splitter/src/contract.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ mod execute {
250250
nodes::events::{
251251
common::{GenericDistributeRewardsEvent, GenericWithdrawRewardsEvent, RestakingEvent},
252252
splitter::{
253-
DistributeRewardsEvent, ForceUnstakeEvent, StakeEvent, WithdrawRewardsEvent,
253+
DistributeRewardsEvent, ForceUnstakeEvent, ReleaseStakeEvent, StakeEvent,
254+
WithdrawRewardsEvent,
254255
},
255256
},
256257
};
@@ -399,7 +400,11 @@ mod execute {
399400
for (consumer, _security_level) in consumer_security_map {
400401
let mut output = OUTPUTS.load(deps.storage, (consumer, &denom))?;
401402
output.amount = output.amount.checked_sub(amount)?;
402-
OUTPUTS.save(deps.storage, (consumer, &denom), &output)?;
403+
if output.amount.is_zero() {
404+
OUTPUTS.remove(deps.storage, (consumer, &denom));
405+
} else {
406+
OUTPUTS.save(deps.storage, (consumer, &denom), &output)?;
407+
}
403408

404409
messages.push(consumer::force_unstake(consumer, &staker, &denom, amount)?)
405410
}
@@ -472,17 +477,61 @@ mod execute {
472477
}
473478

474479
pub fn release_stake(
475-
_deps: DepsMut,
480+
deps: DepsMut,
476481
_env: Env,
477482
info: MessageInfo,
478-
_staker: String,
479-
_amount: Uint128,
480-
_denom: String,
483+
staker: String,
484+
amount: Uint128,
485+
denom: String,
481486
) -> Result<Response, ContractError> {
482487
nonpayable(&info)?;
488+
let staker = deps.api.addr_validate(&staker)?;
489+
let releasing_consumer = info.sender;
490+
491+
// Update input tally
492+
let mut input = INPUTS.load(deps.storage, &denom)?;
493+
input.amount = input.amount.checked_sub(amount)?;
494+
// Do not remove, because we hold other commitment info here
495+
INPUTS.save(deps.storage, &denom, &input)?;
496+
497+
// Force unstake for all other consumers
498+
let config = CONFIG.load(deps.storage)?;
499+
let mut msgs = vec![];
500+
501+
let map = create_consumer_security_map(&config.flows, &denom);
502+
503+
for (consumer, _) in map {
504+
if consumer != releasing_consumer {
505+
msgs.push(consumer::force_unstake(consumer, &staker, &denom, amount)?);
506+
}
507+
508+
// Update their output tallies
509+
let mut output = OUTPUTS.load(deps.storage, (consumer, &denom))?;
510+
output.amount = output.amount.checked_sub(amount)?;
511+
if output.amount.is_zero() {
512+
OUTPUTS.remove(deps.storage, (consumer, &denom));
513+
} else {
514+
OUTPUTS.save(deps.storage, (consumer, &denom), &output)?;
515+
}
516+
}
517+
518+
// Add release message to provider
519+
msgs.push(provider::release_stake(
520+
&config.provider,
521+
&staker,
522+
&denom,
523+
amount,
524+
)?);
525+
526+
let event = ReleaseStakeEvent {
527+
common: RestakingEvent {
528+
staker_addr: staker,
529+
amount,
530+
denom,
531+
},
532+
};
483533

484-
// TODO: add event for it
485-
todo!("From Meeting: Logic here is tricky, so let's save this for later")
534+
Ok(Response::new().add_messages(msgs).add_event(event.into()))
486535
}
487536

488537
pub fn distribute_rewards(

contracts/splitter/src/tests/common.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,3 +1399,167 @@ where
13991399
let total_power_response = weighting.total_power_at_height(None).unwrap();
14001400
assert_eq!(total_power_response.power, power_response_2.power);
14011401
}
1402+
1403+
pub fn test_release_stake<C>(chain: C, denom: String)
1404+
where
1405+
C: CwEnv + AltSigner,
1406+
C::Sender: Addressable,
1407+
{
1408+
let commitment = Commitment {
1409+
denom: denom.clone(),
1410+
security: SecurityLevel {
1411+
max_penalty_percent: 25,
1412+
unbonding_period: 1000,
1413+
},
1414+
};
1415+
1416+
// Create 4 flows that all use the same reference token
1417+
let flows = vec![
1418+
SplitFlow {
1419+
consumer: nodes::Counterparty::Caller {},
1420+
shares: vec![commitment.clone()],
1421+
},
1422+
SplitFlow {
1423+
consumer: nodes::Counterparty::Caller {},
1424+
shares: vec![commitment.clone()],
1425+
},
1426+
SplitFlow {
1427+
consumer: nodes::Counterparty::Caller {},
1428+
shares: vec![commitment.clone()],
1429+
},
1430+
SplitFlow {
1431+
consumer: nodes::Counterparty::Caller {},
1432+
shares: vec![commitment.clone()],
1433+
},
1434+
];
1435+
1436+
// Set up contracts
1437+
let weighting = TokenWeightingContract::new(TOKEN_WEIGHTING_CONTRACT_ID, chain.clone());
1438+
weighting.upload().unwrap();
1439+
1440+
let staking = TokenStakingContract::new("staking", chain.clone());
1441+
staking.upload().unwrap();
1442+
1443+
let mut fan_in = FanInContract::new(FAN_IN_CONTRACT_ID, chain.clone());
1444+
fan_in.upload().unwrap();
1445+
1446+
let splitter = SplitterContract::new(SPLITTER_CONTRACT_ID, chain.clone());
1447+
splitter.upload().unwrap();
1448+
1449+
// Configure flows with weighting contracts
1450+
let mut configured_flows = flows;
1451+
for flow in configured_flows.iter_mut() {
1452+
let terms = RelativeWeights {
1453+
reference: commitment.clone(),
1454+
secondary: vec![], // No secondary tokens in this test
1455+
};
1456+
1457+
flow.consumer = nodes::Counterparty::Create {
1458+
code_id: weighting.code_id().unwrap(),
1459+
msg: serde_json::to_value(token_weighting::InstantiateMsg {
1460+
terms,
1461+
reward_denoms: None,
1462+
provider: nodes::Counterparty::Caller {},
1463+
slasher: None,
1464+
admin: None,
1465+
})
1466+
.unwrap(),
1467+
label: "Token Weighting".to_string(),
1468+
admin: nodes::ContractAdmin::None(),
1469+
}
1470+
}
1471+
1472+
// Instantiate the splitter with fan-in as provider
1473+
let msg = InstantiateMsg {
1474+
provider: nodes::Counterparty::Create {
1475+
code_id: fan_in.code_id().unwrap(),
1476+
msg: serde_json::to_value(fan_in::InstantiateMsg {
1477+
manager: chain.sender().to_string(),
1478+
consumer: nodes::Counterparty::Caller {},
1479+
})
1480+
.unwrap(),
1481+
label: "Fan In".to_string(),
1482+
admin: nodes::ContractAdmin::None(),
1483+
},
1484+
flows: configured_flows,
1485+
};
1486+
splitter.instantiate(&msg, None, None).unwrap();
1487+
1488+
// Set up fan-in
1489+
let config = splitter.config().unwrap();
1490+
let provider = Addr::unchecked(config.provider);
1491+
fan_in.set_default_address(&provider);
1492+
1493+
// Set up staking contract
1494+
staking
1495+
.instantiate(
1496+
&rstk_token_staking::msg::InstantiateMsg {
1497+
commitment: commitment.clone(),
1498+
consumer: nodes::Counterparty::Existing {
1499+
contract: fan_in.addr_str().unwrap(),
1500+
},
1501+
},
1502+
None,
1503+
None,
1504+
)
1505+
.unwrap();
1506+
1507+
fan_in
1508+
.add_input(staking.addr_str().unwrap(), commitment.clone(), None)
1509+
.unwrap();
1510+
1511+
// Get consumers for testing
1512+
let consumers_response = splitter.consumers().unwrap();
1513+
assert_eq!(consumers_response.consumers.len(), 4);
1514+
1515+
// Initial stake
1516+
staking.stake(&coins(1000, &denom)).unwrap();
1517+
chain.next_block().unwrap();
1518+
1519+
// Verify initial power for all consumers
1520+
for consumer_info in consumers_response.consumers.iter() {
1521+
weighting.set_address(&Addr::unchecked(&consumer_info.consumer));
1522+
1523+
let power = weighting
1524+
.voting_power_at_height(chain.sender(), None)
1525+
.unwrap();
1526+
assert_eq!(power.power, Uint128::new(1000));
1527+
1528+
let total_power = weighting.total_power_at_height(None).unwrap();
1529+
assert_eq!(total_power.power, Uint128::new(1000));
1530+
}
1531+
1532+
// Unstake and verify power update
1533+
staking.unstake(coin(400, denom.clone())).unwrap();
1534+
chain.next_block().unwrap();
1535+
1536+
// Verify updated power for all consumers
1537+
for consumer_info in consumers_response.consumers.iter() {
1538+
weighting.set_address(&Addr::unchecked(&consumer_info.consumer));
1539+
1540+
let power = weighting
1541+
.voting_power_at_height(chain.sender(), None)
1542+
.unwrap();
1543+
assert_eq!(power.power, Uint128::new(600));
1544+
1545+
let total_power = weighting.total_power_at_height(None).unwrap();
1546+
assert_eq!(total_power.power, Uint128::new(600));
1547+
}
1548+
1549+
// Complete unstaking process
1550+
chain.wait_seconds(1001).unwrap();
1551+
staking.withdraw_unbonded(None).unwrap();
1552+
1553+
// Final verification that power remains correct
1554+
for consumer_info in consumers_response.consumers.iter() {
1555+
weighting.set_address(&Addr::unchecked(&consumer_info.consumer));
1556+
1557+
let power = weighting
1558+
.voting_power_at_height(chain.sender(), None)
1559+
.unwrap();
1560+
assert_eq!(power.power, Uint128::new(600));
1561+
1562+
let total_power = weighting.total_power_at_height(None).unwrap();
1563+
assert_eq!(total_power.power, Uint128::new(600));
1564+
}
1565+
}

contracts/splitter/src/tests/multi.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,14 @@ fn test_rewards_flow() {
213213
assert_eq!(final_balance, Uint128::new(3000));
214214
}
215215
}
216+
217+
#[test]
218+
fn release_stake() {
219+
let chain = MockBech32::new(BECH_PREFIX);
220+
221+
chain
222+
.add_balance(&chain.sender, coins(10000, DENOM))
223+
.unwrap();
224+
225+
super::common::test_release_stake(chain, DENOM.to_string());
226+
}

packages/apis/src/nodes/events/splitter.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,39 @@ impl From<DistributeRewardsEvent> for Event {
189189
}
190190
}
191191

192+
#[derive(Debug, Clone, PartialEq)]
193+
pub struct ReleaseStakeEvent {
194+
pub common: RestakingEvent,
195+
}
196+
197+
impl TypedEvent for ReleaseStakeEvent {
198+
const NAME: &'static str = "splitter_release_stake_event";
199+
}
200+
201+
impl TryFrom<&Event> for ReleaseStakeEvent {
202+
type Error = EventError;
203+
204+
fn try_from(event: &Event) -> Result<Self, EventError> {
205+
if !Self::is_type(&event.ty) {
206+
return Err(EventError::IncorrectEventType {
207+
expected: Self::NAME.to_string(),
208+
actual: event.clone().ty,
209+
});
210+
}
211+
212+
let common = RestakingEvent::try_from(event)?;
213+
214+
Ok(Self { common })
215+
}
216+
}
217+
218+
impl From<ReleaseStakeEvent> for Event {
219+
fn from(value: ReleaseStakeEvent) -> Self {
220+
let event = Event::new(ReleaseStakeEvent::NAME);
221+
value.common.add_to_event(event)
222+
}
223+
}
224+
192225
#[cfg(test)]
193226
mod tests {
194227
use super::*;

0 commit comments

Comments
 (0)