soroban_env_host/builtin_contracts/
account_contract.rs

1// This is a built-in account 'contract'. This is not actually a contract, as
2// it doesn't need to be directly invoked. But semantically this is analagous
3// to a generic smart wallet contract that supports authentication and blanket
4// context authorization.
5use crate::{
6    auth::{AuthorizedFunction, AuthorizedInvocation},
7    builtin_contracts::{
8        base_types::{Address, BytesN, Vec as HostVec},
9        common_types::ContractExecutable,
10        contract_error::ContractError,
11    },
12    err,
13    host::{
14        frame::{CallParams, ContractReentryMode},
15        Host,
16    },
17    xdr::{
18        self, AccountId, ContractId, ContractIdPreimage, ScErrorCode, ScErrorType,
19        ThresholdIndexes, Uint256,
20    },
21    Env, EnvBase, ErrorHandler, HostError, Symbol, TryFromVal, TryIntoVal, Val,
22};
23use core::cmp::Ordering;
24
25const MAX_ACCOUNT_SIGNATURES: u32 = 20;
26
27use soroban_builtin_sdk_macros::contracttype;
28
29pub const ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME: &str = "__check_auth";
30
31#[derive(Clone)]
32#[contracttype]
33pub(crate) struct ContractAuthorizationContext {
34    pub(crate) contract: Address,
35    pub(crate) fn_name: Symbol,
36    pub(crate) args: HostVec,
37}
38
39#[derive(Clone)]
40#[contracttype]
41pub(crate) struct CreateContractHostFnContext {
42    pub(crate) executable: ContractExecutable,
43    pub(crate) salt: BytesN<32>,
44}
45
46#[derive(Clone)]
47#[contracttype]
48pub(crate) struct CreateContractWithConstructorHostFnContext {
49    pub(crate) executable: ContractExecutable,
50    pub(crate) salt: BytesN<32>,
51    pub(crate) constructor_args: HostVec,
52}
53
54#[derive(Clone)]
55#[contracttype]
56pub(crate) enum AuthorizationContext {
57    Contract(ContractAuthorizationContext),
58    CreateContractHostFn(CreateContractHostFnContext),
59    CreateContractWithCtorHostFn(CreateContractWithConstructorHostFnContext),
60}
61
62#[derive(Clone)]
63#[contracttype]
64pub(crate) struct AccountEd25519Signature {
65    pub(crate) public_key: BytesN<32>,
66    pub(crate) signature: BytesN<64>,
67}
68
69impl AuthorizationContext {
70    fn from_authorized_fn(host: &Host, function: &AuthorizedFunction) -> Result<Self, HostError> {
71        match function {
72            AuthorizedFunction::ContractFn(contract_fn) => {
73                let args = HostVec::try_from_val(host, &contract_fn.args)?;
74                Ok(AuthorizationContext::Contract(
75                    ContractAuthorizationContext {
76                        contract: contract_fn.contract_address.try_into_val(host)?,
77                        fn_name: contract_fn.function_name,
78                        args,
79                    },
80                ))
81            }
82            AuthorizedFunction::CreateContractHostFn(args) => {
83                let wasm_hash = match &args.executable {
84                    xdr::ContractExecutable::Wasm(wasm_hash) => {
85                        BytesN::<32>::from_slice(host, wasm_hash.as_slice())?
86                    }
87                    xdr::ContractExecutable::StellarAsset => return Err(host.err(
88                        ScErrorType::Auth,
89                        ScErrorCode::InvalidInput,
90                        "StellarAsset executable is not allowed when authorizing create_contract host fn",
91                        &[],
92                    )),
93                };
94                let salt = match &args.contract_id_preimage {
95                    ContractIdPreimage::Address(id_from_addr) => {
96                        BytesN::<32>::from_slice(host, id_from_addr.salt.as_slice())?
97                    }
98                    ContractIdPreimage::Asset(_) => return Err(host.err(
99                        ScErrorType::Auth,
100                        ScErrorCode::InvalidInput,
101                        "asset preimage is not allowed when authorizing create_contract host fn",
102                        &[],
103                    )),
104                };
105                if args.constructor_args.is_empty() {
106                    Ok(AuthorizationContext::CreateContractHostFn(
107                        CreateContractHostFnContext {
108                            executable: ContractExecutable::Wasm(wasm_hash),
109                            salt,
110                        },
111                    ))
112                } else {
113                    let args_vec = host.scvals_to_val_vec(&args.constructor_args.as_slice())?;
114                    Ok(AuthorizationContext::CreateContractWithCtorHostFn(
115                        CreateContractWithConstructorHostFnContext {
116                            executable: ContractExecutable::Wasm(wasm_hash),
117                            salt,
118                            constructor_args: args_vec.try_into_val(host)?,
119                        },
120                    ))
121                }
122            }
123        }
124    }
125}
126
127// metering: covered
128fn invocation_tree_to_auth_contexts(
129    host: &Host,
130    invocation: &AuthorizedInvocation,
131    out_contexts: &mut HostVec,
132) -> Result<(), HostError> {
133    out_contexts.push(&AuthorizationContext::from_authorized_fn(
134        host,
135        &invocation.function,
136    )?)?;
137    for sub_invocation in &invocation.sub_invocations {
138        invocation_tree_to_auth_contexts(host, sub_invocation, out_contexts)?;
139    }
140    Ok(())
141}
142
143// metering: covered
144pub(crate) fn check_account_contract_auth(
145    host: &Host,
146    account_contract: &ContractId,
147    signature_payload: &[u8; 32],
148    signature: Val,
149    invocation: &AuthorizedInvocation,
150) -> Result<(), HostError> {
151    let payload_obj = host.bytes_new_from_slice(signature_payload)?;
152    let mut auth_context_vec = HostVec::new(host)?;
153    invocation_tree_to_auth_contexts(host, invocation, &mut auth_context_vec)?;
154    Ok(host
155        .call_n_internal(
156            account_contract,
157            ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME.try_into_val(host)?,
158            &[payload_obj.into(), signature, auth_context_vec.into()],
159            CallParams {
160                // Allow self reentry for this function in order to be able to do
161                // wallet admin ops using the auth framework itself.
162                reentry_mode: ContractReentryMode::SelfAllowed,
163                internal_host_call: true,
164                treat_missing_function_as_noop: false,
165            },
166        )?
167        .try_into()?)
168}
169
170// metering: covered
171pub(crate) fn check_account_authentication(
172    host: &Host,
173    account_id: AccountId,
174    payload: &[u8],
175    signature: Val,
176) -> Result<(), HostError> {
177    let signatures: HostVec = signature.try_into_val(host)?;
178    // Check if there is too many signatures: there shouldn't be more
179    // signatures then the amount of account signers.
180    let len = signatures.len()?;
181    if len > MAX_ACCOUNT_SIGNATURES {
182        return Err(err!(
183            host,
184            ContractError::AuthenticationError,
185            "too many account signers",
186            len
187        ));
188    }
189    if len == 0 {
190        return Err(host.error(
191            ContractError::AuthenticationError.into(),
192            "no account signatures found",
193            &[],
194        ));
195    }
196    let payload_obj = host.bytes_new_from_slice(payload)?;
197    let account = host.load_account(account_id)?;
198    let mut prev_pk: Option<BytesN<32>> = None;
199    let mut weight = 0u32;
200    for i in 0..len {
201        let sig: AccountEd25519Signature = signatures.get(i)?;
202        // Cannot take multiple signatures from the same key
203        if let Some(prev) = prev_pk {
204            if prev.compare(&sig.public_key)? != Ordering::Less {
205                return Err(err!(
206                    host,
207                    ContractError::AuthenticationError,
208                    "public keys are not ordered",
209                    prev,
210                    sig.public_key
211                ));
212            }
213        }
214
215        host.verify_sig_ed25519(
216            sig.public_key.clone().into(),
217            payload_obj,
218            sig.signature.into(),
219        )?;
220
221        let signer_weight =
222            host.get_signer_weight_from_account(Uint256(sig.public_key.to_array()?), &account)?;
223        // 0 weight indicates that signer doesn't belong to this account. Treat
224        // this as an error to indicate a bug in signatures, even if another
225        // signers would have enough weight.
226        if signer_weight == 0 {
227            return Err(err!(
228                host,
229                ContractError::AuthenticationError,
230                "signer does not belong to account",
231                sig.public_key
232            ));
233        }
234        // Overflow isn't possible here as
235        // 255 * MAX_ACCOUNT_SIGNATURES is < u32::MAX,
236        // but to future-proof the code we do saturating_add.
237        weight = weight.saturating_add(signer_weight as u32);
238        prev_pk = Some(sig.public_key);
239    }
240    // This should always work but again, we err on side
241    // of future-proofing against changed assumptions.
242    let Some(threshold) = account.thresholds.0.get(ThresholdIndexes::Med as usize) else {
243        return Err(host.error(
244            (ScErrorType::Auth, ScErrorCode::InternalError).into(),
245            "unexpected thresholds-array size",
246            &[],
247        ));
248    };
249    if weight < *threshold as u32 {
250        Err(err!(
251            host,
252            ContractError::AuthenticationError,
253            "signature weight is lower than threshold",
254            weight,
255            *threshold as u32
256        ))
257    } else {
258        Ok(())
259    }
260}