staking_cli/
delegation.rs1use alloy::{
2 network::Ethereum,
3 primitives::{utils::format_ether, Address, U256},
4 providers::{PendingTransactionBuilder, Provider},
5};
6use anyhow::{bail, Result};
7use hotshot_contract_adapter::{
8 evm::DecodeRevert as _,
9 sol_types::{
10 EspToken::{self, EspTokenErrors},
11 StakeTableV2::{self, StakeTableV2Errors},
12 },
13 stake_table::StakeTableContractVersion,
14};
15
16pub async fn approve(
17 provider: impl Provider,
18 token_addr: Address,
19 stake_table_address: Address,
20 amount: U256,
21) -> Result<PendingTransactionBuilder<Ethereum>> {
22 tracing::info!(
23 "approve {} ESP for {stake_table_address}",
24 format_ether(amount)
25 );
26 let token = EspToken::new(token_addr, provider);
27 token
28 .approve(stake_table_address, amount)
29 .send()
30 .await
31 .maybe_decode_revert::<EspTokenErrors>()
32}
33
34pub async fn delegate(
35 provider: impl Provider,
36 stake_table: Address,
37 validator_address: Address,
38 amount: U256,
39) -> Result<PendingTransactionBuilder<Ethereum>> {
40 tracing::info!(
41 "delegate {} ESP to {validator_address}",
42 format_ether(amount)
43 );
44 let st = StakeTableV2::new(stake_table, provider);
45
46 let version: StakeTableContractVersion = st.getVersion().call().await?.try_into()?;
47 if let StakeTableContractVersion::V2 = version {
48 let min_amount = st.minDelegateAmount().call().await?;
49 if amount < min_amount {
50 bail!(
51 "delegation amount {} ESP is below minimum of {} ESP",
52 format_ether(amount),
53 format_ether(min_amount)
54 );
55 }
56 }
57
58 st.delegate(validator_address, amount)
59 .send()
60 .await
61 .maybe_decode_revert::<StakeTableV2Errors>()
62}
63
64pub async fn undelegate(
65 provider: impl Provider,
66 stake_table: Address,
67 validator_address: Address,
68 amount: U256,
69) -> Result<PendingTransactionBuilder<Ethereum>> {
70 tracing::info!(
71 "undelegate {} ESP from {validator_address}",
72 format_ether(amount)
73 );
74 let st = StakeTableV2::new(stake_table, provider);
75 st.undelegate(validator_address, amount)
76 .send()
77 .await
78 .maybe_decode_revert::<StakeTableV2Errors>()
79}
80
81#[cfg(test)]
82mod test {
83 use alloy::primitives::utils::parse_ether;
84 use rstest::rstest;
85
86 use super::*;
87 use crate::{deploy::TestSystem, receipt::ReceiptExt};
88
89 #[rstest]
90 #[case(StakeTableContractVersion::V1)]
91 #[case(StakeTableContractVersion::V2)]
92 #[tokio::test]
93 async fn test_delegate(#[case] version: StakeTableContractVersion) -> Result<()> {
94 let system = TestSystem::deploy_version(version).await?;
95 system.register_validator().await?;
96 let validator_address = system.deployer_address;
97
98 let amount = parse_ether("1.23")?;
99 let receipt = delegate(
100 &system.provider,
101 system.stake_table,
102 validator_address,
103 amount,
104 )
105 .await?
106 .assert_success()
107 .await?;
108
109 let event = receipt.decoded_log::<StakeTableV2::Delegated>().unwrap();
110 assert_eq!(event.validator, validator_address);
111 assert_eq!(event.amount, amount);
112
113 Ok(())
114 }
115
116 #[rstest]
117 #[case(StakeTableContractVersion::V1)]
118 #[case(StakeTableContractVersion::V2)]
119 #[tokio::test]
120 async fn test_undelegate(#[case] version: StakeTableContractVersion) -> Result<()> {
121 let system = TestSystem::deploy_version(version).await?;
122 let amount = parse_ether("1.23")?;
123 system.register_validator().await?;
124 system.delegate(amount).await?;
125
126 let validator_address = system.deployer_address;
127 let receipt = undelegate(
128 &system.provider,
129 system.stake_table,
130 validator_address,
131 amount,
132 )
133 .await?
134 .assert_success()
135 .await?;
136
137 match version {
138 StakeTableContractVersion::V1 => {
139 let event = receipt.decoded_log::<StakeTableV2::Undelegated>().unwrap();
140 assert_eq!(event.validator, validator_address);
141 assert_eq!(event.amount, amount);
142 },
143 StakeTableContractVersion::V2 => {
144 let event = receipt
145 .decoded_log::<StakeTableV2::UndelegatedV2>()
146 .unwrap();
147 assert_eq!(event.validator, validator_address);
148 assert_eq!(event.amount, amount);
149 let block = system
150 .provider
151 .get_block_by_number(receipt.block_number.unwrap().into())
152 .await?
153 .unwrap();
154 let expected_unlock = block.header.timestamp + system.exit_escrow_period.as_secs();
155 assert_eq!(event.unlocksAt, U256::from(expected_unlock));
156 },
157 }
158
159 Ok(())
160 }
161
162 #[tokio::test]
163 async fn test_delegate_below_minimum_amount() -> Result<()> {
164 let system = TestSystem::deploy().await?;
165 system.register_validator().await?;
166 let validator_address = system.deployer_address;
167
168 let amount = U256::from(123);
169 let err = delegate(
170 &system.provider,
171 system.stake_table,
172 validator_address,
173 amount,
174 )
175 .await
176 .expect_err("should fail with amount below minimum");
177
178 let err_msg = err.to_string();
179 assert!(
180 err_msg.contains("below minimum"),
181 "error should mention below minimum: {err_msg}"
182 );
183 assert!(
184 err_msg.contains("1.000000000000000000 ESP"),
185 "error should include min amount: {err_msg}"
186 );
187
188 Ok(())
189 }
190}