schemata/
ifa.rs

1// RGB schemas
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2025 by
6//     Stefano Pellegrini <stefano.pellegrini@bitfinex.com>
7//
8// Copyright (C) 2025 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22//! Inflatable Fungible Assets (IFA) schema.
23//! (!) Not safe to use in a production environment!
24
25use aluvm::isa::Instr;
26use aluvm::library::{Lib, LibSite};
27use amplify::confinement::Confined;
28use rgbstd::contract::{
29    AssignmentsFilter, ContractData, FungibleAllocation, IssuerWrapper, RightsAllocation,
30    SchemaWrapper,
31};
32use rgbstd::persistence::{ContractStateRead, MemContract};
33use rgbstd::schema::{
34    AssignmentDetails, FungibleType, GenesisSchema, GlobalStateSchema, Occurrences,
35    OwnedStateSchema, Schema, TransitionSchema,
36};
37use rgbstd::stl::{rgb_contract_stl, AssetSpec, ContractTerms, RejectListUrl, StandardTypes};
38use rgbstd::validation::Scripts;
39use rgbstd::vm::RgbIsa;
40use rgbstd::{rgbasm, Amount, GlobalDetails, MetaDetails, SchemaId, TransitionDetails};
41use strict_types::TypeSystem;
42
43use crate::{
44    ERRNO_INFLATION_EXCEEDS_ALLOWANCE, ERRNO_INFLATION_MISMATCH, ERRNO_ISSUED_MISMATCH,
45    ERRNO_NON_EQUAL_IN_OUT, ERRNO_REPLACE_HIDDEN_BURN, ERRNO_REPLACE_NO_INPUT, GS_ISSUED_SUPPLY,
46    GS_MAX_SUPPLY, GS_NOMINAL, GS_REJECT_LIST_URL, GS_TERMS, MS_ALLOWED_INFLATION, OS_ASSET,
47    OS_INFLATION, OS_REPLACE, TS_BURN, TS_INFLATION, TS_REPLACE, TS_TRANSFER,
48};
49
50pub const IFA_SCHEMA_ID: SchemaId = SchemaId::from_array([
51    0x82, 0x65, 0x7f, 0x89, 0x08, 0x2f, 0x06, 0x27, 0x64, 0xdc, 0x04, 0x7c, 0xbb, 0xff, 0xad, 0x94,
52    0x2a, 0x82, 0x30, 0xc0, 0x41, 0xbc, 0xa3, 0x16, 0x43, 0x05, 0xba, 0x24, 0xc5, 0x95, 0xb4, 0x60,
53]);
54
55pub(crate) fn ifa_lib_genesis() -> Lib {
56    #[allow(clippy::diverging_sub_expression)]
57    let code = rgbasm! {
58        // Set common offsets
59        put     a8[1],0;
60        put     a16[0],0;
61
62        // Check reported issued supply against sum of asset allocations in output
63        put     a8[0],ERRNO_ISSUED_MISMATCH;  // set errno
64        ldg     GS_ISSUED_SUPPLY,a8[1],s16[0];  // read issued supply global state
65        extr    s16[0],a64[0],a16[0];  // and store it in a64[0]
66        sas     OS_ASSET;  // check sum of assets assignments in output equals a64[0]
67        test;
68
69        // Check that sum of inflation rights = max supply - issued supply
70        put     a8[0],ERRNO_INFLATION_MISMATCH;  // set errno
71        ldg     GS_MAX_SUPPLY,a8[1],s16[1];  // read max supply global state
72        extr    s16[1],a64[1],a16[0];  // and store it in a64[1]
73        sub.uc  a64[1],a64[0];  // issued supply is still in a64[0], result overwrites a64[0]
74        test;  // fails if result is <0
75        sas     OS_INFLATION;  // check sum of inflation rights in output equals a64[0]
76        test;
77
78        ret;
79    };
80    Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code)
81        .expect("wrong inflatable asset genesis valdiation script")
82}
83
84pub(crate) fn ifa_lib_transfer() -> Lib {
85    let code = rgbasm! {
86        // Checking that the sum of inputs is equal to the sum of outputs
87        put     a8[0],ERRNO_NON_EQUAL_IN_OUT;  // set errno
88        svs     OS_ASSET;  // verify sum
89        test;  // check it didn't fail
90        svs     OS_INFLATION;  // verify sum
91        test;  // check it didn't fail
92
93        // Replace rights validation
94        cnp     OS_REPLACE,a16[0];  // count input replace rights
95        cns     OS_REPLACE,a16[1];  // count output replace rights
96        // Check if input count is 0
97        put     a16[2],0;  // store 0 in a16[2]
98        eq.n    a16[0],a16[2];  // check if input_count == 0
99        // TODO: fix comment
100        jif     40;  // jump to 0x28 if input_count == 0
101        // Input count > 0, check that output count >= input count
102        put     a8[0],ERRNO_REPLACE_HIDDEN_BURN;  // set errno
103        lt.u    a16[1],a16[0];  // output_count < input_count
104        inv     st0;  // output_count >= input_count
105        test;  // fail if output_count < input_count
106        ret;  // return execution flow
107        // 0x28: Input count is 0, output count must also be 0
108        put     a8[0],ERRNO_REPLACE_NO_INPUT;  // set errno
109        eq.n    a16[1],a16[0];  // check if output_count == input_count
110        test;  // fail if output_count != input_count (=0)
111        ret;  // return execution flow
112    };
113    Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code).expect("wrong transfer validation script")
114}
115
116pub(crate) fn ifa_lib_inflation() -> Lib {
117    #[allow(clippy::diverging_sub_expression)]
118    let code = rgbasm! {
119        // Set common offsets
120        put     a8[1],0;
121        put     a16[0],0;
122
123        // Check reported issued supply equals sum of asset allocations in output
124        put     a8[0],ERRNO_ISSUED_MISMATCH;  // set errno
125        ldg     GS_ISSUED_SUPPLY,a8[1],s16[0];  // read issued supply global state
126        extr    s16[0],a64[0],a16[0];  // and store it in a64[0]
127        sas     OS_ASSET;  // check sum of asset allocations in output equals issued_supply
128        test;
129        cpy     a64[0],a64[1];  // store issued supply in a64[1] for later
130
131        // Check reported allowed inflation equals sum of inflation rights in output
132        put     a8[0],ERRNO_INFLATION_MISMATCH;  // set errno
133        ldm     MS_ALLOWED_INFLATION,s16[0];  // read allowed inflation global state
134        extr    s16[0],a64[0],a16[0];  // and store it in a64[0]
135        sas     OS_INFLATION;  // check sum of inflation rights in output equals a64[0]
136        test;
137
138        // Check that input inflation rights equals issued supply + allowed inflation
139        put     a8[0],ERRNO_INFLATION_EXCEEDS_ALLOWANCE;
140        add.uc  a64[1],a64[0];  // result is stored in a64[0]
141        test;  // fails in case of an overflow
142        sps     OS_INFLATION;  // check sum of inflation rights in input equals a64[0]
143        test;
144
145        ret;
146    };
147    Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code).expect("wrong inflation validation script")
148}
149
150fn ifa_standard_types() -> StandardTypes { StandardTypes::with(rgb_contract_stl()) }
151
152fn ifa_schema() -> Schema {
153    let types = ifa_standard_types();
154
155    let alu_id_transfer = ifa_lib_transfer().id();
156
157    Schema {
158        ffv: zero!(),
159        name: tn!("InflatableFungibleAsset"),
160        meta_types: tiny_bmap! {
161            MS_ALLOWED_INFLATION => MetaDetails {
162                sem_id: types.get("RGBContract.Amount"),
163                name: fname!("allowedInflation"),
164            }
165        },
166        global_types: tiny_bmap! {
167            GS_NOMINAL => GlobalDetails {
168                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.AssetSpec")),
169                name: fname!("spec"),
170            },
171            GS_TERMS => GlobalDetails {
172                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.ContractTerms")),
173                name: fname!("terms"),
174            },
175            GS_ISSUED_SUPPLY => GlobalDetails {
176                global_state_schema: GlobalStateSchema::many(types.get("RGBContract.Amount")),
177                name: fname!("issuedSupply"),
178            },
179            GS_MAX_SUPPLY => GlobalDetails {
180                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.Amount")),
181                name: fname!("maxSupply"),
182            },
183            GS_REJECT_LIST_URL => GlobalDetails {
184                global_state_schema: GlobalStateSchema::once(types.get("RGBContract.RejectListUrl")),
185                name: fname!("rejectListUrl"),
186            },
187        },
188        owned_types: tiny_bmap! {
189            OS_ASSET => AssignmentDetails {
190                owned_state_schema: OwnedStateSchema::Fungible(FungibleType::Unsigned64Bit),
191                name: fname!("assetOwner"),
192                default_transition: TS_TRANSFER,
193            },
194            OS_INFLATION => AssignmentDetails {
195                owned_state_schema: OwnedStateSchema::Fungible(FungibleType::Unsigned64Bit),
196                name: fname!("inflationAllowance"),
197                default_transition: TS_TRANSFER
198            },
199            OS_REPLACE => AssignmentDetails {
200                owned_state_schema: OwnedStateSchema::Declarative,
201                name: fname!("replaceRight"),
202                default_transition: TS_TRANSFER,
203            }
204        },
205        genesis: GenesisSchema {
206            metadata: none!(),
207            globals: tiny_bmap! {
208                GS_NOMINAL => Occurrences::Once,
209                GS_TERMS => Occurrences::Once,
210                GS_ISSUED_SUPPLY => Occurrences::Once,
211                GS_MAX_SUPPLY => Occurrences::Once,
212                GS_REJECT_LIST_URL => Occurrences::NoneOrOnce,
213            },
214            assignments: tiny_bmap! {
215                OS_ASSET => Occurrences::NoneOrMore,
216                OS_INFLATION => Occurrences::NoneOrMore,
217                OS_REPLACE => Occurrences::NoneOrMore,
218            },
219            validator: Some(LibSite::with(0, ifa_lib_genesis().id())),
220        },
221        transitions: tiny_bmap! {
222            TS_TRANSFER => TransitionDetails {
223                transition_schema: TransitionSchema {
224                    metadata: none!(),
225                    globals: none!(),
226                    inputs: tiny_bmap! {
227                        OS_ASSET => Occurrences::NoneOrMore,
228                        OS_INFLATION => Occurrences::NoneOrMore,
229                        OS_REPLACE => Occurrences::NoneOrMore
230                    },
231                    assignments: tiny_bmap! {
232                        OS_ASSET => Occurrences::NoneOrMore,
233                        OS_INFLATION => Occurrences::NoneOrMore,
234                        OS_REPLACE => Occurrences::NoneOrMore
235                    },
236                    validator: Some(LibSite::with(0, alu_id_transfer))
237                },
238                name: fname!("transfer"),
239            },
240            TS_INFLATION => TransitionDetails {
241                transition_schema: TransitionSchema {
242                    metadata: tiny_bset![MS_ALLOWED_INFLATION],
243                    globals: tiny_bmap! {
244                        GS_ISSUED_SUPPLY => Occurrences::Once,
245                    },
246                    inputs: tiny_bmap! {
247                        OS_INFLATION => Occurrences::OnceOrMore
248                    },
249                    assignments: tiny_bmap! {
250                        OS_ASSET => Occurrences::OnceOrMore,
251                        OS_INFLATION => Occurrences::NoneOrMore
252                    },
253                    validator: Some(LibSite::with(0, ifa_lib_inflation().id()))
254                },
255                name: fname!("inflate"),
256            },
257            TS_BURN => TransitionDetails {
258                transition_schema: TransitionSchema {
259                    metadata: none!(),
260                    globals: none!(),
261                    inputs: tiny_bmap! {
262                        OS_ASSET => Occurrences::NoneOrMore,
263                        OS_REPLACE => Occurrences::NoneOrMore,
264                        OS_INFLATION => Occurrences::NoneOrMore,
265                    },
266                    assignments: none!(),
267                    validator: None
268                },
269                name: fname!("burn"),
270            },
271            TS_REPLACE => TransitionDetails {
272                transition_schema: TransitionSchema {
273                    metadata: none!(),
274                    globals: none!(),
275                    inputs: tiny_bmap! {
276                        OS_ASSET => Occurrences::OnceOrMore,
277                        OS_REPLACE => Occurrences::OnceOrMore,
278                    },
279                    assignments: tiny_bmap! {
280                        OS_ASSET => Occurrences::OnceOrMore,
281                        OS_REPLACE => Occurrences::OnceOrMore,
282                    },
283                    validator: Some(LibSite::with(0, alu_id_transfer))
284                },
285                name: fname!("replace"),
286            },
287        },
288        default_assignment: Some(OS_ASSET),
289    }
290}
291
292#[derive(Default)]
293pub struct InflatableFungibleAsset;
294
295impl IssuerWrapper for InflatableFungibleAsset {
296    type Wrapper<S: ContractStateRead> = IfaWrapper<S>;
297
298    fn schema() -> Schema { ifa_schema() }
299
300    fn types() -> TypeSystem { ifa_standard_types().type_system(ifa_schema()) }
301
302    fn scripts() -> Scripts {
303        let alu_lib_genesis = ifa_lib_genesis();
304        let alu_id_genesis = alu_lib_genesis.id();
305
306        let alu_lib_transfer = ifa_lib_transfer();
307        let alu_id_transfer = alu_lib_transfer.id();
308
309        let alu_lib_inflation = ifa_lib_inflation();
310        let alu_id_inflation = alu_lib_inflation.id();
311
312        Confined::from_checked(bmap! {
313            alu_id_genesis => alu_lib_genesis,
314            alu_id_transfer => alu_lib_transfer,
315            alu_id_inflation => alu_lib_inflation,
316        })
317    }
318}
319#[derive(Clone, Eq, PartialEq, Debug, From)]
320pub struct IfaWrapper<S: ContractStateRead>(ContractData<S>);
321
322impl<S: ContractStateRead> SchemaWrapper<S> for IfaWrapper<S> {
323    fn with(data: ContractData<S>) -> Self {
324        if data.schema.schema_id() != IFA_SCHEMA_ID {
325            panic!("the provided schema is not IFA");
326        }
327        Self(data)
328    }
329}
330
331impl<S: ContractStateRead> IfaWrapper<S> {
332    pub fn spec(&self) -> AssetSpec {
333        let strict_val = &self
334            .0
335            .global("spec")
336            .next()
337            .expect("IFA requires global state `spec` to have at least one item");
338        AssetSpec::from_strict_val_unchecked(strict_val)
339    }
340
341    pub fn contract_terms(&self) -> ContractTerms {
342        let strict_val = &self
343            .0
344            .global("terms")
345            .next()
346            .expect("IFA requires global state `terms` to have at least one item");
347        ContractTerms::from_strict_val_unchecked(strict_val)
348    }
349
350    pub fn reject_list_url(&self) -> Option<RejectListUrl> {
351        self.0
352            .global("rejectListUrl")
353            .next()
354            .map(|strict_val| RejectListUrl::from_strict_val_unchecked(&strict_val))
355    }
356
357    fn issued_supply(&self) -> impl Iterator<Item = Amount> + '_ {
358        self.0
359            .global("issuedSupply")
360            .map(|amount| Amount::from_strict_val_unchecked(&amount))
361    }
362
363    pub fn total_issued_supply(&self) -> Amount { self.issued_supply().sum() }
364
365    pub fn issuance_amounts(&self) -> Vec<Amount> { self.issued_supply().collect::<Vec<_>>() }
366
367    pub fn max_supply(&self) -> Amount {
368        self.0
369            .global("maxSupply")
370            .map(|amount| Amount::from_strict_val_unchecked(&amount))
371            .sum()
372    }
373
374    pub fn allocations<'c>(
375        &'c self,
376        filter: impl AssignmentsFilter + 'c,
377    ) -> impl Iterator<Item = FungibleAllocation> + 'c {
378        self.0.fungible_raw(OS_ASSET, filter).unwrap()
379    }
380
381    pub fn inflation_allocations<'c>(
382        &'c self,
383        filter: impl AssignmentsFilter + 'c,
384    ) -> impl Iterator<Item = FungibleAllocation> + 'c {
385        self.0.fungible_raw(OS_INFLATION, filter).unwrap()
386    }
387
388    pub fn replace_rights<'c>(
389        &'c self,
390        filter: impl AssignmentsFilter + 'c,
391    ) -> impl Iterator<Item = RightsAllocation> + 'c {
392        self.0.rights_raw(OS_REPLACE, filter).unwrap()
393    }
394}
395
396#[cfg(test)]
397mod test {
398    use crate::ifa::ifa_schema;
399    use crate::IFA_SCHEMA_ID;
400
401    #[test]
402    fn schema_id() {
403        let schema_id = ifa_schema().schema_id();
404        eprintln!("{:#04x?}", schema_id.to_byte_array());
405        assert_eq!(IFA_SCHEMA_ID, schema_id);
406    }
407}