staking_cli/
delegation.rs

1use 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}