pallet_evm_precompile_dapp_staking/
lib.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19//! Astar dApp staking interface.
20
21#![cfg_attr(not(feature = "std"), no_std)]
22
23use fp_evm::PrecompileHandle;
24use frame_system::pallet_prelude::BlockNumberFor;
25use num_enum::{IntoPrimitive, TryFromPrimitive};
26use parity_scale_codec::MaxEncodedLen;
27
28use frame_support::{
29    dispatch::{GetDispatchInfo, PostDispatchInfo},
30    ensure,
31    traits::{ConstU32, IsType},
32};
33
34use pallet_evm::AddressMapping;
35use precompile_utils::{
36    prelude::*,
37    solidity::{
38        codec::{Reader, Writer},
39        Codec,
40    },
41};
42use sp_core::{Get, H160, U256};
43use sp_runtime::traits::{Dispatchable, Zero};
44use sp_std::{marker::PhantomData, prelude::*};
45extern crate alloc;
46
47use astar_primitives::{dapp_staking::SmartContractHandle, AccountId, Balance, BlockNumber};
48use pallet_dapp_staking::{
49    AccountLedgerFor, ActiveProtocolState, ContractStake, ContractStakeAmount, CurrentEraInfo,
50    DAppInfoFor, EraInfo, EraRewardSpanFor, EraRewards, IntegratedDApps, Ledger,
51    Pallet as DAppStaking, ProtocolState, SingularStakingInfo, StakerInfo, Subperiod,
52};
53
54pub const STAKER_BYTES_LIMIT: u32 = 32;
55type GetStakerBytesLimit = ConstU32<STAKER_BYTES_LIMIT>;
56
57pub type DynamicAddress = BoundedBytes<GetStakerBytesLimit>;
58
59#[cfg(test)]
60mod test;
61
62/// Helper struct used to encode protocol state.
63#[derive(Debug, Clone, solidity::Codec)]
64pub(crate) struct PrecompileProtocolState {
65    era: U256,
66    period: U256,
67    subperiod: u8,
68}
69
70/// Helper struct used to encode different smart contract types for the v2 interface.
71#[derive(Debug, Clone, solidity::Codec)]
72pub struct SmartContractV2 {
73    contract_type: SmartContractTypes,
74    address: DynamicAddress,
75}
76
77/// Convenience type for smart contract type handling.
78#[derive(Clone, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
79#[repr(u8)]
80pub(crate) enum SmartContractTypes {
81    Evm,
82    Wasm,
83}
84
85impl Codec for SmartContractTypes {
86    fn read(reader: &mut Reader) -> MayRevert<SmartContractTypes> {
87        let value256: U256 = reader
88            .read()
89            .map_err(|_| RevertReason::read_out_of_bounds(Self::signature()))?;
90
91        let value_as_u8: u8 = value256
92            .try_into()
93            .map_err(|_| RevertReason::value_is_too_large(Self::signature()))?;
94
95        value_as_u8
96            .try_into()
97            .map_err(|_| RevertReason::custom("Unknown smart contract type").into())
98    }
99
100    fn write(writer: &mut Writer, value: Self) {
101        let value_as_u8: u8 = value.into();
102        U256::write(writer, value_as_u8.into());
103    }
104
105    fn has_static_size() -> bool {
106        true
107    }
108
109    fn signature() -> String {
110        "uint8".into()
111    }
112}
113
114pub struct DappStakingV3Precompile<R>(PhantomData<R>);
115#[precompile_utils::precompile]
116impl<R> DappStakingV3Precompile<R>
117where
118    R: pallet_evm::Config
119        + pallet_dapp_staking::Config
120        + frame_system::Config<AccountId = AccountId>,
121    BlockNumberFor<R>: IsType<BlockNumber>,
122    <R::RuntimeCall as Dispatchable>::RuntimeOrigin: From<Option<R::AccountId>>,
123    <R as pallet_evm::Config>::AddressMapping: AddressMapping<R::AccountId>,
124    R::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
125    R::RuntimeCall: From<pallet_dapp_staking::Call<R>>,
126{
127    // v1 functions
128
129    /// Read the ongoing `era` number.
130    #[precompile::public("read_current_era()")]
131    #[precompile::view]
132    fn read_current_era(handle: &mut impl PrecompileHandle) -> EvmResult<U256> {
133        // TODO: benchmark this function so we can measure ref time & PoV correctly
134        // Storage item: ActiveProtocolState:
135        // Twox64(8) + ProtocolState::max_encoded_len
136        handle.record_db_read::<R>(8 + ProtocolState::max_encoded_len())?;
137
138        let current_era = ActiveProtocolState::<R>::get().era();
139
140        Ok(current_era.into())
141    }
142
143    /// Read the `unbonding period` or `unlocking period` expressed in the number of eras.
144    #[precompile::public("read_unbonding_period()")]
145    #[precompile::view]
146    fn read_unbonding_period(_: &mut impl PrecompileHandle) -> EvmResult<U256> {
147        // constant, no DB read
148        Ok(<R as pallet_dapp_staking::Config>::UnlockingPeriod::get().into())
149    }
150
151    /// Read the total assigned reward pool for the given era.
152    ///
153    /// Total amount is sum of staker & dApp rewards.
154    #[precompile::public("read_era_reward(uint32)")]
155    #[precompile::view]
156    fn read_era_reward(handle: &mut impl PrecompileHandle, era: u32) -> EvmResult<u128> {
157        // TODO: benchmark this function so we can measure ref time & PoV correctly
158        // Storage item: EraRewards:
159        // Twox64Concat(8) + EraIndex(4) + EraRewardSpanFor::max_encoded_len
160        handle.record_db_read::<R>(12 + EraRewardSpanFor::<R>::max_encoded_len())?;
161
162        // Get the appropriate era reward span
163        let era_span_index = DAppStaking::<R>::era_reward_span_index(era);
164        let reward_span = EraRewards::<R>::get(&era_span_index).unwrap_or_default();
165
166        // Sum up staker & dApp reward pools for the era
167        let reward = reward_span.get(era).map_or(Zero::zero(), |r| {
168            r.staker_reward_pool().saturating_add(r.dapp_reward_pool())
169        });
170
171        Ok(reward)
172    }
173
174    /// Read the total staked amount for the given era.
175    ///
176    /// In case era is very far away in history, it's possible that the information is not available.
177    /// In that case, zero is returned.
178    ///
179    /// This is safe to use for current era and the next one.
180    #[precompile::public("read_era_staked(uint32)")]
181    #[precompile::view]
182    fn read_era_staked(handle: &mut impl PrecompileHandle, era: u32) -> EvmResult<u128> {
183        // TODO: benchmark this function so we can measure ref time & PoV correctly
184        // Storage item: ActiveProtocolState:
185        // Twox64(8) + ProtocolState::max_encoded_len
186        handle.record_db_read::<R>(8 + ProtocolState::max_encoded_len())?;
187
188        let current_era = ActiveProtocolState::<R>::get().era();
189
190        // There are few distinct scenarios:
191        // 1. Era is in the past so the value might exist.
192        // 2. Era is current or the next one, in which case we definitely have that information.
193        // 3. Era is from the future (more than the next era), in which case we don't have that information.
194        if era < current_era {
195            // TODO: benchmark this function so we can measure ref time & PoV correctly
196            // Storage item: EraRewards:
197            // Twox64Concat(8) + Twox64Concat(8 + EraIndex(4)) + EraRewardSpanFor::max_encoded_len
198            handle.record_db_read::<R>(20 + EraRewardSpanFor::<R>::max_encoded_len())?;
199
200            let era_span_index = DAppStaking::<R>::era_reward_span_index(era);
201            let reward_span = EraRewards::<R>::get(&era_span_index).unwrap_or_default();
202
203            let staked = reward_span.get(era).map_or(Zero::zero(), |r| r.staked());
204
205            Ok(staked.into())
206        } else if era == current_era || era == current_era.saturating_add(1) {
207            // TODO: benchmark this function so we can measure ref time & PoV correctly
208            // Storage item: CurrentEraInfo:
209            // Twox64Concat(8) + EraInfo::max_encoded_len
210            handle.record_db_read::<R>(8 + EraInfo::max_encoded_len())?;
211
212            let current_era_info = CurrentEraInfo::<R>::get();
213
214            if era == current_era {
215                Ok(current_era_info.current_stake_amount().total())
216            } else {
217                Ok(current_era_info.next_stake_amount().total())
218            }
219        } else {
220            Err(RevertReason::custom("Era is in the future").into())
221        }
222    }
223
224    /// Read the total staked amount by the given account.
225    #[precompile::public("read_staked_amount(bytes)")]
226    #[precompile::view]
227    fn read_staked_amount(
228        handle: &mut impl PrecompileHandle,
229        staker: DynamicAddress,
230    ) -> EvmResult<u128> {
231        // TODO: benchmark this function so we can measure ref time & PoV correctly
232        // Storage item: ActiveProtocolState:
233        // Twox64(8) + ProtocolState::max_encoded_len
234        // Storage item: Ledger:
235        // Blake2_128Concat(16 + SmartContract::max_encoded_len) + Ledger::max_encoded_len
236        handle.record_db_read::<R>(
237            24 + AccountLedgerFor::<R>::max_encoded_len()
238                + ProtocolState::max_encoded_len()
239                + <R as pallet_dapp_staking::Config>::SmartContract::max_encoded_len(),
240        )?;
241
242        let staker = Self::parse_input_address(staker.into())?;
243
244        // read the account's ledger
245        let ledger = Ledger::<R>::get(&staker);
246        log::trace!(target: "ds-precompile", "read_staked_amount for account: {:?}, ledger: {:?}", staker, ledger);
247
248        // Make sure to check staked amount against the ongoing period (past period stakes are reset to zero).
249        let current_period_number = ActiveProtocolState::<R>::get().period_number();
250
251        Ok(ledger.staked_amount(current_period_number))
252    }
253
254    /// Read the total staked amount by the given staker on the given contract.
255    #[precompile::public("read_staked_amount_on_contract(address,bytes)")]
256    #[precompile::view]
257    fn read_staked_amount_on_contract(
258        handle: &mut impl PrecompileHandle,
259        contract_h160: Address,
260        staker: DynamicAddress,
261    ) -> EvmResult<u128> {
262        // TODO: benchmark this function so we can measure ref time & PoV correctly
263        // Storage item: ActiveProtocolState:
264        // Twox64(8) + ProtocolState::max_encoded_len
265        // Storage item: StakerInfo:
266        // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len
267        handle.record_db_read::<R>(
268            24 + ProtocolState::max_encoded_len()
269                + <R as pallet_dapp_staking::Config>::SmartContract::max_encoded_len()
270                + SingularStakingInfo::max_encoded_len(),
271        )?;
272
273        let smart_contract =
274            <R as pallet_dapp_staking::Config>::SmartContract::evm(contract_h160.into());
275
276        // parse the staker account
277        let staker = Self::parse_input_address(staker.into())?;
278
279        // Get staking info for the staker/contract combination
280        let staking_info = StakerInfo::<R>::get(&staker, &smart_contract).unwrap_or_default();
281        log::trace!(target: "ds-precompile", "read_staked_amount_on_contract for account:{:?}, staking_info: {:?}", staker, staking_info);
282
283        // Ensure that the staking info is checked against the current period (stakes from past periods are reset)
284        let current_period_number = ActiveProtocolState::<R>::get().period_number();
285
286        if staking_info.period_number() == current_period_number {
287            Ok(staking_info.total_staked_amount())
288        } else {
289            Ok(0_u128)
290        }
291    }
292
293    /// Read the total amount staked on the given contract right now.
294    #[precompile::public("read_contract_stake(address)")]
295    #[precompile::view]
296    fn read_contract_stake(
297        handle: &mut impl PrecompileHandle,
298        contract_h160: Address,
299    ) -> EvmResult<u128> {
300        // TODO: benchmark this function so we can measure ref time & PoV correctly
301        // Storage item: ActiveProtocolState:
302        // Twox64(8) + ProtocolState::max_encoded_len
303        // Storage item: IntegratedDApps:
304        // Blake2_128Concat(16 + SmartContract::max_encoded_len) + DAppInfoFor::max_encoded_len
305        // Storage item: ContractStake:
306        // Twox64Concat(8) + EraIndex(4) + ContractStakeAmount::max_encoded_len
307        handle.record_db_read::<R>(
308            36 + ProtocolState::max_encoded_len()
309                + <R as pallet_dapp_staking::Config>::SmartContract::max_encoded_len()
310                + DAppInfoFor::<R>::max_encoded_len()
311                + ContractStakeAmount::max_encoded_len(),
312        )?;
313
314        let smart_contract =
315            <R as pallet_dapp_staking::Config>::SmartContract::evm(contract_h160.into());
316
317        let current_period_number = ActiveProtocolState::<R>::get().period_number();
318        let dapp_info = match IntegratedDApps::<R>::get(&smart_contract) {
319            Some(dapp_info) => dapp_info,
320            None => {
321                // If the contract is not registered, return 0 to keep the legacy behavior.
322                return Ok(0_u128);
323            }
324        };
325
326        // call pallet-dapps-staking
327        let contract_stake = ContractStake::<R>::get(&dapp_info.id());
328
329        Ok(contract_stake.total_staked_amount(current_period_number))
330    }
331
332    /// Register contract with the dapp-staking pallet
333    /// Register is root origin only. This should always fail when called via evm precompile.
334    #[precompile::public("register(address)")]
335    fn register(_: &mut impl PrecompileHandle, _address: Address) -> EvmResult<bool> {
336        // register is root-origin call. it should always fail when called via evm precompiles.
337        Err(RevertReason::custom("register via evm precompile is not allowed").into())
338    }
339
340    /// Lock & stake some amount on the specified contract.
341    ///
342    /// In case existing `stakeable` is sufficient to cover the given `amount`, only the `stake` operation is performed.
343    /// Otherwise, best effort is done to lock the additional amount so `stakeable` amount can cover the given `amount`.
344    #[precompile::public("bond_and_stake(address,uint128)")]
345    fn bond_and_stake(
346        handle: &mut impl PrecompileHandle,
347        contract_h160: Address,
348        amount: u128,
349    ) -> EvmResult<bool> {
350        // TODO: benchmark this function so we can measure ref time & PoV correctly
351        // Storage item: ActiveProtocolState:
352        // Twox64(8) + ProtocolState::max_encoded_len
353        // Storage item: Ledger:
354        // Blake2_128Concat(16 + SmartContract::max_encoded_len()) + Ledger::max_encoded_len
355        handle.record_db_read::<R>(
356            24 + AccountLedgerFor::<R>::max_encoded_len()
357                + ProtocolState::max_encoded_len()
358                + <R as pallet_dapp_staking::Config>::SmartContract::max_encoded_len(),
359        )?;
360
361        let smart_contract =
362            <R as pallet_dapp_staking::Config>::SmartContract::evm(contract_h160.into());
363        log::trace!(target: "ds-precompile", "bond_and_stake {:?}, {:?}", smart_contract, amount);
364
365        // Read total locked & staked amounts
366        let origin = R::AddressMapping::into_account_id(handle.context().caller);
367        let protocol_state = ActiveProtocolState::<R>::get();
368        let ledger = Ledger::<R>::get(&origin);
369
370        // Check if stakeable amount is enough to cover the given `amount`
371        let stakeable_amount = ledger.stakeable_amount(protocol_state.period_number());
372
373        // If it isn't, we need to first lock the additional amount.
374        if stakeable_amount < amount {
375            let delta = amount.saturating_sub(stakeable_amount);
376
377            let lock_call = pallet_dapp_staking::Call::<R>::lock { amount: delta };
378            RuntimeHelper::<R>::try_dispatch(handle, Some(origin.clone()).into(), lock_call, 0)?;
379        }
380
381        // Now, with best effort, we can try & stake the given `value`.
382        let stake_call = pallet_dapp_staking::Call::<R>::stake {
383            smart_contract,
384            amount,
385        };
386        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), stake_call, 0)?;
387
388        Ok(true)
389    }
390
391    /// Start unbonding process and unstake balance from the contract.
392    #[precompile::public("unbond_and_unstake(address,uint128)")]
393    fn unbond_and_unstake(
394        handle: &mut impl PrecompileHandle,
395        contract_h160: Address,
396        amount: u128,
397    ) -> EvmResult<bool> {
398        // TODO: benchmark this function so we can measure ref time & PoV correctly
399        // Storage item: ActiveProtocolState:
400        // Twox64(8) + ProtocolState::max_encoded_len
401        // Storage item: StakerInfo:
402        // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len
403        handle.record_db_read::<R>(
404            24 + ProtocolState::max_encoded_len()
405                + <R as pallet_dapp_staking::Config>::SmartContract::max_encoded_len()
406                + SingularStakingInfo::max_encoded_len(),
407        )?;
408
409        let smart_contract =
410            <R as pallet_dapp_staking::Config>::SmartContract::evm(contract_h160.into());
411        let origin = R::AddressMapping::into_account_id(handle.context().caller);
412        log::trace!(target: "ds-precompile", "unbond_and_unstake {:?}, {:?}", smart_contract, amount);
413
414        // Find out if there is something staked on the contract
415        let protocol_state = ActiveProtocolState::<R>::get();
416        let staker_info = StakerInfo::<R>::get(&origin, &smart_contract).unwrap_or_default();
417
418        // If there is, we need to unstake it before calling `unlock`
419        if staker_info.period_number() == protocol_state.period_number() {
420            let unstake_call = pallet_dapp_staking::Call::<R>::unstake {
421                smart_contract,
422                amount,
423            };
424            RuntimeHelper::<R>::try_dispatch(handle, Some(origin.clone()).into(), unstake_call, 0)?;
425        }
426
427        // Now we can try and `unlock` the given `amount`
428        let unlock_call = pallet_dapp_staking::Call::<R>::unlock { amount };
429        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), unlock_call, 0)?;
430
431        Ok(true)
432    }
433
434    /// Claim back the unbonded (or unlocked) funds.
435    #[precompile::public("withdraw_unbonded()")]
436    fn withdraw_unbonded(handle: &mut impl PrecompileHandle) -> EvmResult<bool> {
437        let origin = R::AddressMapping::into_account_id(handle.context().caller);
438        let call = pallet_dapp_staking::Call::<R>::claim_unlocked {};
439
440        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), call, 0)?;
441
442        Ok(true)
443    }
444
445    /// Claim dApp rewards for the given era
446    #[precompile::public("claim_dapp(address,uint128)")]
447    fn claim_dapp(
448        handle: &mut impl PrecompileHandle,
449        contract_h160: Address,
450        era: u128,
451    ) -> EvmResult<bool> {
452        let smart_contract =
453            <R as pallet_dapp_staking::Config>::SmartContract::evm(contract_h160.into());
454
455        // parse era
456        let era = era
457            .try_into()
458            .map_err::<Revert, _>(|_| RevertReason::value_is_too_large("era type").into())
459            .in_field("era")?;
460
461        log::trace!(target: "ds-precompile", "claim_dapp {:?}, era {:?}", smart_contract, era);
462
463        let origin = R::AddressMapping::into_account_id(handle.context().caller);
464        let call = pallet_dapp_staking::Call::<R>::claim_dapp_reward {
465            smart_contract,
466            era,
467        };
468
469        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), call, 0)?;
470
471        Ok(true)
472    }
473
474    /// Claim staker rewards.
475    ///
476    /// Smart contract argument is legacy & is ignored in the new implementation.
477    #[precompile::public("claim_staker(address)")]
478    fn claim_staker(
479        handle: &mut impl PrecompileHandle,
480        _contract_h160: Address,
481    ) -> EvmResult<bool> {
482        let origin = R::AddressMapping::into_account_id(handle.context().caller);
483        let call = pallet_dapp_staking::Call::<R>::claim_staker_rewards {};
484
485        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), call, 0)?;
486
487        Ok(true)
488    }
489
490    /// Set claim reward destination for the caller.
491    ///
492    /// This call has been deprecated by dApp staking v3.
493    #[precompile::public("set_reward_destination(uint8)")]
494    fn set_reward_destination(_: &mut impl PrecompileHandle, _destination: u8) -> EvmResult<bool> {
495        Err(RevertReason::custom("Setting reward destination is no longer supported.").into())
496    }
497
498    /// Withdraw staked funds from the unregistered contract
499    #[precompile::public("withdraw_from_unregistered(address)")]
500    fn withdraw_from_unregistered(
501        handle: &mut impl PrecompileHandle,
502        contract_h160: Address,
503    ) -> EvmResult<bool> {
504        let smart_contract =
505            <R as pallet_dapp_staking::Config>::SmartContract::evm(contract_h160.into());
506        log::trace!(target: "ds-precompile", "withdraw_from_unregistered {:?}", smart_contract);
507
508        let origin = R::AddressMapping::into_account_id(handle.context().caller);
509        let call = pallet_dapp_staking::Call::<R>::unstake_from_unregistered { smart_contract };
510
511        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), call, 0)?;
512
513        Ok(true)
514    }
515
516    /// Transfers stake from one contract to another.
517    /// This is a legacy functionality that is no longer supported via direct call to dApp staking v3.
518    /// However, it can be achieved by chaining `unstake` and `stake` calls.
519    #[precompile::public("nomination_transfer(address,uint128,address)")]
520    fn nomination_transfer(
521        handle: &mut impl PrecompileHandle,
522        origin_contract_h160: Address,
523        amount: u128,
524        target_contract_h160: Address,
525    ) -> EvmResult<bool> {
526        // TODO: benchmark this function so we can measure ref time & PoV correctly
527        // Storage item: StakerInfo:
528        // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len
529        handle.record_db_read::<R>(
530            16 + <R as pallet_dapp_staking::Config>::SmartContract::max_encoded_len()
531                + SingularStakingInfo::max_encoded_len(),
532        )?;
533
534        let origin_smart_contract =
535            <R as pallet_dapp_staking::Config>::SmartContract::evm(origin_contract_h160.into());
536        let target_smart_contract =
537            <R as pallet_dapp_staking::Config>::SmartContract::evm(target_contract_h160.into());
538        log::trace!(target: "ds-precompile", "nomination_transfer {:?} {:?} {:?}", origin_smart_contract, amount, target_smart_contract);
539
540        // Find out how much staker has staked on the origin contract
541        let origin = R::AddressMapping::into_account_id(handle.context().caller);
542        let staker_info = StakerInfo::<R>::get(&origin, &origin_smart_contract).unwrap_or_default();
543
544        // We don't care from which period the staked amount is, the logic takes care of the situation
545        // if value comes from the past period.
546        let staked_amount = staker_info.total_staked_amount();
547        let minimum_allowed_stake_amount =
548            <R as pallet_dapp_staking::Config>::MinimumStakeAmount::get();
549
550        // In case the remaining staked amount on the origin contract is less than the minimum allowed stake amount,
551        // everything will be unstaked. To keep in line with legacy `nomination_transfer` behavior, we should transfer
552        // the entire amount from the origin to target contract.
553        //
554        // In case value comes from the past period, we don't care, since the `unstake` call will fall apart.
555        let stake_amount = if staked_amount > 0
556            && staked_amount.saturating_sub(amount) < minimum_allowed_stake_amount
557        {
558            staked_amount
559        } else {
560            amount
561        };
562
563        // First call unstake from the origin smart contract
564        let unstake_call = pallet_dapp_staking::Call::<R>::unstake {
565            smart_contract: origin_smart_contract,
566            amount,
567        };
568        RuntimeHelper::<R>::try_dispatch(handle, Some(origin.clone()).into(), unstake_call, 0)?;
569
570        // Then call stake on the target smart contract
571        let stake_call = pallet_dapp_staking::Call::<R>::stake {
572            smart_contract: target_smart_contract,
573            amount: stake_amount,
574        };
575        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), stake_call, 0)?;
576
577        Ok(true)
578    }
579
580    // v2 functions
581
582    /// Read the current protocol state.
583    #[precompile::public("protocol_state()")]
584    #[precompile::view]
585    fn protocol_state(handle: &mut impl PrecompileHandle) -> EvmResult<PrecompileProtocolState> {
586        // TODO: benchmark this function so we can measure ref time & PoV correctly
587        // Storage item: ActiveProtocolState:
588        // Twox64(8) + ProtocolState::max_encoded_len
589        handle.record_db_read::<R>(8 + ProtocolState::max_encoded_len())?;
590
591        let protocol_state = ActiveProtocolState::<R>::get();
592
593        Ok(PrecompileProtocolState {
594            era: protocol_state.era().into(),
595            period: protocol_state.period_number().into(),
596            subperiod: subperiod_id(&protocol_state.subperiod()),
597        })
598    }
599
600    /// Read the `unbonding period` or `unlocking period` expressed in the number of eras.
601    #[precompile::public("unlocking_period()")]
602    #[precompile::view]
603    fn unlocking_period(_: &mut impl PrecompileHandle) -> EvmResult<U256> {
604        // constant, no DB read
605        Ok(DAppStaking::<R>::unlocking_period().into())
606    }
607
608    /// Attempt to lock the given amount into the dApp staking protocol.
609    #[precompile::public("lock(uint128)")]
610    fn lock(handle: &mut impl PrecompileHandle, amount: u128) -> EvmResult<bool> {
611        // Prepare call & dispatch it
612        let origin = R::AddressMapping::into_account_id(handle.context().caller);
613        let lock_call = pallet_dapp_staking::Call::<R>::lock { amount };
614        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), lock_call, 0)?;
615
616        Ok(true)
617    }
618
619    /// Attempt to unlock the given amount from the dApp staking protocol.
620    #[precompile::public("unlock(uint128)")]
621    fn unlock(handle: &mut impl PrecompileHandle, amount: u128) -> EvmResult<bool> {
622        // Prepare call & dispatch it
623        let origin = R::AddressMapping::into_account_id(handle.context().caller);
624        let unlock_call = pallet_dapp_staking::Call::<R>::unlock { amount };
625        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), unlock_call, 0)?;
626
627        Ok(true)
628    }
629
630    /// Attempts to claim unlocking chunks which have undergone the entire unlocking period.
631    #[precompile::public("claim_unlocked()")]
632    fn claim_unlocked(handle: &mut impl PrecompileHandle) -> EvmResult<bool> {
633        // Prepare call & dispatch it
634        let origin = R::AddressMapping::into_account_id(handle.context().caller);
635        let claim_unlocked_call = pallet_dapp_staking::Call::<R>::claim_unlocked {};
636        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), claim_unlocked_call, 0)?;
637
638        Ok(true)
639    }
640
641    /// Attempts to stake the given amount on the given smart contract.
642    #[precompile::public("stake((uint8,bytes),uint128)")]
643    fn stake(
644        handle: &mut impl PrecompileHandle,
645        smart_contract: SmartContractV2,
646        amount: Balance,
647    ) -> EvmResult<bool> {
648        let smart_contract = Self::decode_smart_contract(smart_contract)?;
649
650        // Prepare call & dispatch it
651        let origin = R::AddressMapping::into_account_id(handle.context().caller);
652        let stake_call = pallet_dapp_staking::Call::<R>::stake {
653            smart_contract,
654            amount,
655        };
656        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), stake_call, 0)?;
657
658        Ok(true)
659    }
660
661    /// Attempts to unstake the given amount from the given smart contract.
662    #[precompile::public("unstake((uint8,bytes),uint128)")]
663    fn unstake(
664        handle: &mut impl PrecompileHandle,
665        smart_contract: SmartContractV2,
666        amount: Balance,
667    ) -> EvmResult<bool> {
668        let smart_contract = Self::decode_smart_contract(smart_contract)?;
669
670        // Prepare call & dispatch it
671        let origin = R::AddressMapping::into_account_id(handle.context().caller);
672        let unstake_call = pallet_dapp_staking::Call::<R>::unstake {
673            smart_contract,
674            amount,
675        };
676        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), unstake_call, 0)?;
677
678        Ok(true)
679    }
680
681    /// Attempts to claim one or more pending staker rewards.
682    #[precompile::public("claim_staker_rewards()")]
683    fn claim_staker_rewards(handle: &mut impl PrecompileHandle) -> EvmResult<bool> {
684        // Prepare call & dispatch it
685        let origin = R::AddressMapping::into_account_id(handle.context().caller);
686        let claim_staker_rewards_call = pallet_dapp_staking::Call::<R>::claim_staker_rewards {};
687        RuntimeHelper::<R>::try_dispatch(
688            handle,
689            Some(origin).into(),
690            claim_staker_rewards_call,
691            0,
692        )?;
693
694        Ok(true)
695    }
696
697    /// Attempts to claim a bonus reward for maintaining an eligible bonus status with the given dApp.
698    #[precompile::public("claim_bonus_reward((uint8,bytes))")]
699    fn claim_bonus_reward(
700        handle: &mut impl PrecompileHandle,
701        smart_contract: SmartContractV2,
702    ) -> EvmResult<bool> {
703        let smart_contract = Self::decode_smart_contract(smart_contract)?;
704
705        // Prepare call & dispatch it
706        let origin = R::AddressMapping::into_account_id(handle.context().caller);
707        let claim_bonus_reward_call =
708            pallet_dapp_staking::Call::<R>::claim_bonus_reward { smart_contract };
709        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), claim_bonus_reward_call, 0)?;
710
711        Ok(true)
712    }
713
714    /// Attempts to claim dApp reward for the given dApp in the given era.
715    #[precompile::public("claim_bonus_reward((uint8,bytes),uint256)")]
716    fn claim_dapp_reward(
717        handle: &mut impl PrecompileHandle,
718        smart_contract: SmartContractV2,
719        era: U256,
720    ) -> EvmResult<bool> {
721        let smart_contract = Self::decode_smart_contract(smart_contract)?;
722        let era = era
723            .try_into()
724            .map_err::<Revert, _>(|_| RevertReason::value_is_too_large("Era number.").into())
725            .in_field("era")?;
726
727        // Prepare call & dispatch it
728        let origin = R::AddressMapping::into_account_id(handle.context().caller);
729        let claim_dapp_reward_call = pallet_dapp_staking::Call::<R>::claim_dapp_reward {
730            smart_contract,
731            era,
732        };
733        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), claim_dapp_reward_call, 0)?;
734
735        Ok(true)
736    }
737
738    /// Attempts to unstake everything from an unregistered contract.
739    #[precompile::public("unstake_from_unregistered((uint8,bytes))")]
740    fn unstake_from_unregistered(
741        handle: &mut impl PrecompileHandle,
742        smart_contract: SmartContractV2,
743    ) -> EvmResult<bool> {
744        let smart_contract = Self::decode_smart_contract(smart_contract)?;
745
746        // Prepare call & dispatch it
747        let origin = R::AddressMapping::into_account_id(handle.context().caller);
748        let unstake_from_unregistered_call =
749            pallet_dapp_staking::Call::<R>::unstake_from_unregistered { smart_contract };
750        RuntimeHelper::<R>::try_dispatch(
751            handle,
752            Some(origin).into(),
753            unstake_from_unregistered_call,
754            0,
755        )?;
756
757        Ok(true)
758    }
759
760    /// Attempts to cleanup expired entries for the staker.
761    #[precompile::public("cleanup_expired_entries()")]
762    fn cleanup_expired_entries(handle: &mut impl PrecompileHandle) -> EvmResult<bool> {
763        // Prepare call & dispatch it
764        let origin = R::AddressMapping::into_account_id(handle.context().caller);
765        let cleanup_expired_entries_call =
766            pallet_dapp_staking::Call::<R>::cleanup_expired_entries {};
767        RuntimeHelper::<R>::try_dispatch(
768            handle,
769            Some(origin).into(),
770            cleanup_expired_entries_call,
771            0,
772        )?;
773
774        Ok(true)
775    }
776
777    #[precompile::public("move_stake((uint8,bytes),(uint8,bytes),uint128)")]
778    fn move_stake(
779        handle: &mut impl PrecompileHandle,
780        source_contract: SmartContractV2,
781        destination_contract: SmartContractV2,
782        amount: Balance,
783    ) -> EvmResult<bool> {
784        let source_contract = Self::decode_smart_contract(source_contract)?;
785        let destination_contract = Self::decode_smart_contract(destination_contract)?;
786
787        // Prepare call & dispatch it
788        let origin = R::AddressMapping::into_account_id(handle.context().caller);
789        let move_call = pallet_dapp_staking::Call::<R>::move_stake {
790            source_contract,
791            destination_contract,
792            amount,
793        };
794        RuntimeHelper::<R>::try_dispatch(handle, Some(origin).into(), move_call, 0)?;
795
796        Ok(true)
797    }
798
799    // Utility functions
800
801    /// Helper method to decode smart contract struct for v2 calls
802    pub(crate) fn decode_smart_contract(
803        smart_contract: SmartContractV2,
804    ) -> EvmResult<<R as pallet_dapp_staking::Config>::SmartContract> {
805        let smart_contract = match smart_contract.contract_type {
806            SmartContractTypes::Evm => {
807                ensure!(
808                    smart_contract.address.as_bytes().len() == 20,
809                    revert("Invalid address length for Astar EVM smart contract.")
810                );
811                let h160_address = H160::from_slice(smart_contract.address.as_bytes());
812                <R as pallet_dapp_staking::Config>::SmartContract::evm(h160_address)
813            }
814            SmartContractTypes::Wasm => {
815                ensure!(
816                    smart_contract.address.as_bytes().len() == 32,
817                    revert("Invalid address length for Astar WASM smart contract.")
818                );
819                let mut staker_bytes = [0_u8; 32];
820                staker_bytes[..].clone_from_slice(&smart_contract.address.as_bytes());
821
822                <R as pallet_dapp_staking::Config>::SmartContract::wasm(staker_bytes.into())
823            }
824        };
825
826        Ok(smart_contract)
827    }
828
829    /// Helper method to parse H160 or SS58 address
830    pub(crate) fn parse_input_address(staker_vec: Vec<u8>) -> EvmResult<R::AccountId> {
831        let staker: R::AccountId = match staker_vec.len() {
832            // public address of the ss58 account has 32 bytes
833            32 => {
834                let mut staker_bytes = [0_u8; 32];
835                staker_bytes[..].clone_from_slice(&staker_vec[0..32]);
836
837                staker_bytes.into()
838            }
839            // public address of the H160 account has 20 bytes
840            20 => {
841                let mut staker_bytes = [0_u8; 20];
842                staker_bytes[..].clone_from_slice(&staker_vec[0..20]);
843
844                R::AddressMapping::into_account_id(staker_bytes.into())
845            }
846            _ => {
847                // Return err if account length is wrong
848                return Err(revert("Error while parsing staker's address"));
849            }
850        };
851
852        Ok(staker)
853    }
854}
855
856/// Numeric Id of the subperiod enum value.
857pub(crate) fn subperiod_id(subperiod: &Subperiod) -> u8 {
858    match subperiod {
859        Subperiod::Voting => 0,
860        Subperiod::BuildAndEarn => 1,
861    }
862}