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