diff --git a/README.md b/README.md
index ebab5a8f..a986d2c8 100644
--- a/README.md
+++ b/README.md
@@ -164,9 +164,9 @@ N/A
-As mentioned above, the interface of these blueprints differs greatly. Representing this as architecture there are many different ways that this problem of different interfaces can be solved, each of which has its advantages and disadvantages. The following paragraphs discuss some of the ways Ignition could have gone about addressing this problem and then arrive at the final solution that was chosen.
+As mentioned above, the interface of these blueprints differs greatly. Architecturally there are many different ways that this problem of different interfaces can be solved, each of which has its advantages and disadvantages. The following paragraphs discuss some of the ways Ignition could have gone about addressing this problem and then arrive at the final solution that was chosen.
-It is good to start with a naive solution to the problem, examine its strengths and weaknesses and then examine how it can be improved. One way that this problem could be solved is by including logic for handling the different exchanges directly into the core Ignition blueprint, the idea is that Ignition would determine which blueprint the passed component belongs to and then there would be branching logic based on the result. If it is an Ociswap v2 pool then it will perform invocations in one way, and similarly with Caviarnine v1 and Defiplaza v2. The best thing about this approach is that it is simple to implement, simple to understand, and simple to audit and check. However, this approach makes upgradeability and the addition of new exchanges very difficult. Each time a new exchange needs to be added or invocation logic needs to be changed then a completely new Ignition package, with the required added support for the new exchange integration or changes to the existing one, is required that changes the logic and migration of funds from the old component to the new component is needed. Additionally, this ties the Ignition protocol itself very closely to the exchanges which is undesirable.
+It is good to start with a naive solution to the problem, examine its strengths and weaknesses and then examine how it can be improved. One way that this problem could be solved is by including logic for handling the different exchanges directly into the core Ignition blueprint, the idea is that Ignition would determine which blueprint the passed component belongs to and then there would be branching logic based on the result. As an example, if it is an Ociswap v2 pool then it will perform invocations in one way, and similarly with Caviarnine v1 and Defiplaza v2. The best thing about this approach is that it is simple to implement, simple to understand, and simple to audit and check. However, this approach makes upgradeability and the addition of new exchanges very difficult. Each time a new exchange needs to be added or invocation logic needs to be changed then a completely new Ignition package, with the required added support for the new exchange integration or changes to the existing one, is required and migration of funds from the old component to the new component is needed. Additionally, this ties the Ignition protocol itself very closely to the exchanges which is undesirable.
The core problem with the previous naive approach is the fact that the Ignition blueprint became tied to exchanges and their interfaces. The requirement of a new package for any changes that Ignition makes to the supported exchanges or to how the invocations are made is a deal breaker. Therefore since tying them together is undesirable then perhaps splitting them apart would show some desirable characteristics. Therefore, instead of a single blueprint that contains all of the Ignition logic and also the exchange invocations logic, separating them into two separate blueprints or components might be ideal. The first blueprint would be the Ignition protocol blueprint that handles all of the Ignition logic and there would also be an Ignition invocations blueprint that contains all of the code for communicating with the exchanges and it would offer a standardized interface such that the Ignition protocol blueprint becomes fully exchange-agnostic while all of the exchange related logic lives in a separate blueprint. When the Ignition protocol blueprint wishes to invoke a method (e.g., to open a position) on an exchange component it would invoke the Ignition invocations blueprint and that blueprint would handle the invocations accordingly and branching logic accordingly and then return the result. The biggest advantage of this architecture over the previous one is that changing the invocations logic or supporting a new exchange has become significantly simpler, it is as simple as authoring a new Ignition invocations blueprint with whatever changes or additions are required, instantiating a new component from it, and making the Ignition protocol component point to the new invocations component instead of the previous one. Since the invocations blueprint offers a standardized interface then the Ignition protocol component would be able to invoke the new invocations blueprint just fine. The disadvantage of this approach is that the Ignition invocations blueprint is handling many things and this is especially true with how complex the integration logic is. Another disadvantage is that from the point of view of the ledger, any change to the invocation makes the Ignition protocol blueprint point to a completely different invocation blueprint. Meaning that it is difficult to understand from the on-ledger information alone what the change impacts. Therefore, being more granular might be useful.
@@ -177,9 +177,7 @@ Finally, this final architecture is the one chosen for Ignition. Building on the
-The above is a diagram representing the architecture described in the text above. There is a core protocol layer that everything builds on. For an exchange to be integrated an exchange adapter blueprint is written and published, a component is instantiated, and that component is registered into the Ignition protocol as the adapter to use when invocations to a particular exchange are required. There only exists a single adapter component per exchange meaning that if some changes were required to the adapter then all that would change would be the component address of the registered adapter in Ignition.
-
-The Ignition protocol component has a map where it stores a mapping of the `BlueprintId` (that's a package address and a blueprint name) of exchange pools to the following information about the exchange: the liquidity receipt resource to mint when a user opens a position, the component address of the adapter to use for invocations, and the set of component addresses that users are allowed to contribute to. So, for example, if a change is needed to the Ociswap v2 adapter, then the appropriate method will be called on Ignition which would update the component address of the adapter to use on Ociswap v2's entry in this map.
+The above diagram represents the architecture described in the text above. There is a core protocol layer that everything builds on. For an exchange to be integrated an exchange adapter blueprint is written and published, a component is instantiated, and that component is registered into the Ignition protocol as the adapter to use when invocations to a particular exchange are required. There only exists a single adapter component per exchange meaning that if some changes were required to the adapter then all that would change would be the component address of the registered adapter in Ignition. The fact that there are multiple adapters that Ignition has or that an adapter is used to communicate with the oracle or the pools is abstracted away from users of Ignition since the Ignition protocol component is the main component that users interact with and call.
Ignition has a clear separation of concerns between the Ignition protocol and the exchange adapters. The following are the concerns of each of them:
@@ -188,16 +186,16 @@ Ignition has a clear separation of concerns between the Ignition protocol and th
From this point onward in the document _"Ignition protocol"_ refers to the protocol layer of Ignition which is exchange agnostic and _"Ignition system"_ refers to the system as a whole including the protocol and the various adapters.
-One of the core responsibilities of an adapter is to calculate the amount of fees earned on a position between the time that it was opened and closed. In some cases, this calculation might be non-trivial and the adapter might need to store some information about the position when it was first opened somewhere such that it can use this data later on when it closes the position. Since all of the exchanges differ in the way that they work the data that is required to calculate the fees for positions is different between them and does not have a single unified structure. This data is not stored in the adapter's state since that would make upgradeability difficult: moving from one adapter component to another would require migrating this data along with it and if the data is large then this might be difficult to do. Therefore, adapters aim to be as stateless as possible. The only state that it saved on the adapters is state that can either be very cheaply computed or state that must be there. This brings us back to the question: "If such information can not be stored in the adapter's state, then where is it stored?". It is stored in an unstructured field of the liquidity receipt that the user gets back when they open a liquidity position. Thus, when Ignition invokes an adapter for a position to be opened in an exchange the adapter returns the pool units and what the system refers to as the _"adapter-specific information"_. This information is stored in an unstructured field on the `LiquidityReceipt`. It is an unstructured field since all NFTs must have an SBOR schema and each one of the adapters needs to save data of a different structure in this field. Therefore Ignition made this one field unstructured and left the writing, reading, and interpretation of this field to be up to the adapters themselves. Since the adapter has written to this field it also knows how to interpret what it wrote. Making the field unstructured means that this field can take any value, it could be a string, a tuple, an enum, a number, or anything at all. This provides a beautiful abstraction for the data where the higher layer defines the encoding and decoding while the lower layer stores the data not knowing what the data is. This is yet another way that Ignition is built with upgradeability and extensibility in mind with patterns that ensure that moving from one component to another is as seamless as possible.
+The Ignition protocol component has a map where it stores a mapping of the `BlueprintId` (that's a package address and a blueprint name) of exchange pools to the following information about the exchange: the liquidity receipt resource to mint when a user opens a position, the component address of the adapter to use for invocations, and the set of component addresses that users are allowed to contribute to. For example, if a change is needed to the Ociswap v2 adapter, then the appropriate method will be called on Ignition which would update the component address of the adapter to use on Ociswap v2's entry in this map.
-The fact that there are multiple adapters that Ignition has or that an adapter is used to communicate with the oracle or the pools is abstracted away from users of Ignition since the main component that users of Ignition need to invoke is the Ignition protocol component.
+One of the core responsibilities of an adapter is to calculate the amount of fees earned on a position between the time that it was opened and closed. In some cases, this calculation might be non-trivial and the adapter might need to store some information about the position when it was first opened somewhere such that it can use this data later on when it closes the position. Since all of the exchanges differ in the way that they work the data that is required to calculate the fees for positions is different between them and does not have a single unified structure. This data is not stored in the adapter's state since that would make upgradeability difficult: moving from one adapter component to another would require migrating this data along with it and if the data is large then this might be difficult to do. Therefore, adapters aim to be as stateless as possible. The only state that it saved on the adapters is state that can either be very cheaply computed or state that must be there. This brings us back to the question: "If such information can not be stored in the adapter's state, then where is it stored?". It is stored in an unstructured field of the liquidity receipt that the user gets back when they open a liquidity position. Thus, when Ignition invokes an adapter for a position to be opened in an exchange the adapter returns the pool units and what the system refers to as the _"adapter-specific information"_. This information is stored in an unstructured field on the `LiquidityReceipt`. It is an unstructured field since all NFTs must have an SBOR schema and each one of the adapters needs to save data of a different structure in this field. Therefore Ignition made this one field unstructured and left the writing, reading, and interpretation of this field to be up to the adapters themselves. Since the adapter has written to this field it also knows how to interpret what it wrote. Making the field unstructured means that this field can take any value, it could be a string, a tuple, an enum, a number, or anything at all. This provides a beautiful abstraction for the data where the higher layer defines the encoding and decoding while the lower layer stores the data not knowing what the data is. This is yet another way that Ignition is built with upgradeability and extensibility in mind with patterns that ensure that moving from one component to another is as seamless as possible.
The table below examines how Ignition addresses its [technical requirements](#technical-requirements):
| Requirement | Addressed By |
| ----------- | ----------- |
-| All aspects of Ignition must be easily upgradable and modifiable. | In this architecture all aspects are indeed upgradable and modifiable. The adapters uses for any of the exchanges can be changed easily and fund migration to the new component is trivial. |
-| Ignition must support new exchanges trivially. | For a new exchange to be supported it needs an entry in the Ignition exchange integrations map with the adapter to use, the liquidity receipt resource to mint and expect, and the set of all allowed pools. No new package is required, modifications can be made at runtime. |
+| All aspects of Ignition must be easily upgradable and modifiable. | In this architecture all aspects are indeed upgradable and modifiable. The adapter used for any of the exchange can be changed easily and fund migration to the new component is trivial. |
+| Ignition must support new exchanges trivially. | For a new exchange to be supported it needs an entry in the Ignition exchange integrations map with the adapter to use, the liquidity receipt resource to mint and expect, and the set of all allowed pools. No new Ignition protocol package is required, modifications can be made at runtime. |
| Ignition's oracle must be easy to replace. | The oracle integration into Ignition assumes a standardized oracle adapter interface. This means that the oracle can be replaced by simply writing an adapter for the a new oracle provider and making Ignition point to the oracle. |
| Ignition must not be tied to any protocol resource. | Ignition's protocol resource is stored in its state and must be specified when the component is first instantiated. |
| Ignition must control what pools the users are allowed to contribute to. | Ignition's map that stores the exchange integrations information stores this data and Ignition disallows contributions to any pool that is not one of the allowed pools. |
@@ -208,18 +206,18 @@ The table below examines how Ignition addresses its [technical requirements](#te
## Interfaces
-As can be seen in the [architecture](#architecture) section standardized interfaces are a core aspect of the Ignition system that it can not function without. However, there does not currently exist any tooling in the Scrypto toolchain for using interfaces. To be more specific, Ignition's needs with regards to interfaces are:
+As can be seen in the [architecture](#architecture) section standardized interfaces are a core aspect of the Ignition system that it can not function without. To be more specific, Ignition's needs with regards to interfaces are:
-1. The need to be able to define standardized interfaces. This will be used to define the interface of the Pools and Oracle that Ignition invokes.
+1. The need to be able to define standardized interfaces. This will be used to define the interface of the pool and oracle adapters that Ignition invokes.
2. The need for blueprints to implement the defined standardized interface. This will be used in implementing exchange adapters so that they adhere to the defined interface.
3. The ability to type-safely call methods on a component that implements the interface. This will be needed when Ignition invokes a component that it believes to implement some interface.
-One of the core libraries developed for Ignition is the [`scrypto-interface`](./libraries/scrypto-interface/) library which is built to address this exact problem of interfaces and the need for them. This library focuses on users who are more interested in the interface than the implementation, which is commonly referred to as a "has a" versus "is a". The table below explains how the [`scrypto-interface`](./libraries/scrypto-interface/) library addresses the needs that Ignition has.
+However, there does not currently exist any tooling in the Scrypto toolchain that relates to this use of interfaces. One of the core libraries developed for Ignition is the [`scrypto-interface`](./libraries/scrypto-interface/) library which is built to address this exact problem of interfaces and the need for them. This library focuses on users who are more interested in the interface than the implementation, which is commonly referred to as a "has a" versus "is a". The table below explains how the [`scrypto-interface`](./libraries/scrypto-interface/) library addresses the needs that Ignition has.
| Ignition Need | How [`scrypto-interface`](./libraries/scrypto-interface/) Addresses It
| ---- | ---- |
-| The need to be able to define standardized interfaces. This will be used to define the interface of the Pools and Oracle that Ignition invokes. | The library has a macro for defining an interface called the `define_interface` macro which generates out a trait based on the interface and Scrypto, Scrypto-Test, and Manifest Builder stubs. |
-| The need for blueprints to implement the defined standardized interface. This will be used in implementing exchange adapters so that they adhere to the defined interface. | The trait that is generated by the `define_interface` macro can be implemented on blueprints with the aid of the `blueprint_with_traits` macro which is a drop-in replacement for the `blueprint` macro that extends it to allow for traits to be implemented blueprints. This provides compile-time checking of trait implementations and would provide compile-time errors in the event that the interface implementation deviates from the interface definition. |
+| The need to be able to define standardized interfaces. This will be used to define the interface of the pool and oracle adapters that Ignition invokes. | The library has a macro for defining an interface called the `define_interface` macro which generates a trait based on the interface and Scrypto, Scrypto-Test, and Manifest Builder stubs. |
+| The need for blueprints to implement the defined standardized interface. This will be used in implementing exchange adapters so that they adhere to the defined interface. | The trait that is generated by the `define_interface` macro can be implemented on blueprints with the aid of the `blueprint_with_traits` macro which is a drop-in replacement for the `blueprint` macro that extends it to allow for traits to be implemented on blueprints. This provides compile-time checking of trait implementations and would provide compile-time errors in the event that the interface implementation deviates from the interface definition. |
| The ability to type-safely call methods on a component that implements the interface. This will be needed when Ignition invokes a component that it believes to implement some interface. | The `define_interface` macro generates stubs for Scrypto, Scrypto Test, and the Manifest Builder which offers a type-safe way to invoke components implementing the interface regardless of the environment. |
More information on the [`scrypto-interface`](./libraries/scrypto-interface/) library can be found in its [`README.md`](./libraries/scrypto-interface/README.md). Usage of the [`scrypto-interface`](./libraries/scrypto-interface/) library can be found in the [`libraries/ports-interface/src/`](./libraries/ports-interface/src/) which contains the interface definitions of the pool and oracle adapters.
@@ -288,7 +286,7 @@ For the Ignition protocol to open a liquidity position on some pool it needs to
3. `others` - Any other buckets that were returned by the exchange when the liquidity position was opened. This is currently not in use by any of the existing integrations but the Ignition protocol understands this field to be "buckets that should be returned to the caller without any processing" which could be used for exchanges that provide rewards when liquidity positions are opened.
4. `adapter_specific_information` - As discussed in the [architecture](#architecture) section, adapters do not store position data in their state to make upgradeability easier, they instead store that data in an unstructured field on the `LiquidityReceipt` resource minted for users when they open a liquidity position such that a higher layer (the adapter) understands the encoding and decoding and a lower layer (the protocol) handles the storage of the data without doing any interpretation of it. The type used for this is `AnyValue` which as the name suggests, could take any value.
-When closing a liquidity position the Ignition protocol calls the `close_liquidity_position` method on the adapter for that exchange supplying the address of the pool, the pool units, and the adapter-specific information that the adapter wanted the protocol to store when the position was first opened. It is the responsibility of this method to close the liquidity position and to calculate the fees earned on the position between the time for the period that it was open. Therefore, this method returns a `CloseLiquidityPositionOutput`` object containing the following:
+When closing a liquidity position the Ignition protocol calls the `close_liquidity_position` method on the adapter for that exchange supplying the address of the pool, the pool units, and the adapter-specific information that the adapter wanted the protocol to store when the position was first opened. It is the responsibility of this method to close the liquidity position and to calculate the fees earned on the position between the time for the period that it was open. Therefore, this method returns a `CloseLiquidityPositionOutput` object containing the following:
1. `resources` - The resources obtained when closing the liquidity position. Typically this would be the protocol and user resources.
2. `others` - Any other buckets that were returned by the exchange when the liquidity position was opened. This is currently not in use by any of the existing integrations but the Ignition protocol understands this field to be "buckets that should be returned to the caller without any processing" which could be used for exchanges that provide rewards when liquidity positions are closed.
@@ -432,6 +430,8 @@ fn example_test(
}
```
+All stateful tests can be found in the [`testing/stateful-tests/`](./testing/stateful-tests/).
+
## Publishing and Bootstrapping
Ignition is made up of multiple packages, components, and resources which makes the process to follow to get to a running system quite long. Additionally, the modular nature of Ignition means that the process might not always be the same. As an example, one time the Ignition, Ociswap Adapter, Caviarnine Adapter, and Defiplaza Adapter packages might be needed while at other times only the Ignition package might need to be published since it was the only part that changed. The process of publishing and bootstrapping Ignition must be easy and efficient to allow for quick iterations and to quickly be able to get Ignition in the hands of integrators.
diff --git a/testing/stateful-tests/tests/lib.rs b/testing/stateful-tests/tests/lib.rs
index 217f5965..05577e14 100644
--- a/testing/stateful-tests/tests/lib.rs
+++ b/testing/stateful-tests/tests/lib.rs
@@ -194,6 +194,35 @@ macro_rules! define_open_and_close_liquidity_position_tests {
let current_epoch = test_runner.get_current_epoch();
+ test_runner.execute_manifest_without_auth(ManifestBuilder::new()
+ .lock_fee(test_account, dec!(10))
+ .mint_fungible(XRD, dec!(200_000_000_000_000))
+ .take_from_worktop(XRD, dec!(100_000_000_000_000), "volatile")
+ .take_from_worktop(
+ XRD,
+ dec!(100_000_000_000_000),
+ "non_volatile",
+ )
+ .with_name_lookup(|builder, _| {
+ let volatile = builder.bucket("volatile");
+ let non_volatile = builder.bucket("non_volatile");
+
+ builder
+ .call_method(
+ receipt.components.protocol_entities.ignition,
+ "deposit_protocol_resources",
+ (volatile, Volatility::Volatile),
+ )
+ .call_method(
+ receipt.components.protocol_entities.ignition,
+ "deposit_protocol_resources",
+ (non_volatile, Volatility::NonVolatile),
+ )
+ })
+ .build()
+ )
+ .expect_commit_success();
+
// Act
let transaction = TransactionBuilder::new()
.header(TransactionHeaderV1 {
@@ -259,6 +288,35 @@ macro_rules! define_open_and_close_liquidity_position_tests {
let current_epoch = test_runner.get_current_epoch();
+ test_runner.execute_manifest_without_auth(ManifestBuilder::new()
+ .lock_fee(test_account, dec!(10))
+ .mint_fungible(XRD, dec!(200_000_000_000_000))
+ .take_from_worktop(XRD, dec!(100_000_000_000_000), "volatile")
+ .take_from_worktop(
+ XRD,
+ dec!(100_000_000_000_000),
+ "non_volatile",
+ )
+ .with_name_lookup(|builder, _| {
+ let volatile = builder.bucket("volatile");
+ let non_volatile = builder.bucket("non_volatile");
+
+ builder
+ .call_method(
+ receipt.components.protocol_entities.ignition,
+ "deposit_protocol_resources",
+ (volatile, Volatility::Volatile),
+ )
+ .call_method(
+ receipt.components.protocol_entities.ignition,
+ "deposit_protocol_resources",
+ (non_volatile, Volatility::NonVolatile),
+ )
+ })
+ .build()
+ )
+ .expect_commit_success();
+
let transaction = TransactionBuilder::new()
.header(TransactionHeaderV1 {
network_id: 1,
@@ -477,7 +535,7 @@ fn log_reported_price_from_defiplaza_pool(
},
);
receipt.expect_commit_success();
- for i in (0..4) {
+ for i in 0..4 {
let price = receipt.expect_commit_success().output::(i);
println!("{price:#?}");
}
diff --git a/testing/tests/tests/caviarnine_v1_simulation.rs b/testing/tests/tests/caviarnine_v1_simulation.rs
index f3147155..7b46bfab 100644
--- a/testing/tests/tests/caviarnine_v1_simulation.rs
+++ b/testing/tests/tests/caviarnine_v1_simulation.rs
@@ -509,7 +509,7 @@ macro_rules! define_price_test {
paste::paste! {
$(
#[test]
- fn [](
+ fn [](
) {
let env = ScryptoUnitEnv::new();
let pool_information = mainnet_state::pool_information(&env.resources);
@@ -522,7 +522,7 @@ macro_rules! define_price_test {
test_effect_of_price_action_on_fees(- $multiplier, env, pool_information.bitcoin.0, pool_information.bitcoin.1 )
}
#[test]
- fn [](
+ fn [](
) {
let env = ScryptoUnitEnv::new();
let pool_information = mainnet_state::pool_information(&env.resources);
@@ -535,7 +535,7 @@ macro_rules! define_price_test {
test_effect_of_price_action_on_fees(- $multiplier, env, pool_information.ethereum.0, pool_information.ethereum.1 )
}
#[test]
- fn [](
+ fn [](
) {
let env = ScryptoUnitEnv::new();
let pool_information = mainnet_state::pool_information(&env.resources);
@@ -548,7 +548,7 @@ macro_rules! define_price_test {
test_effect_of_price_action_on_fees(- $multiplier, env, pool_information.usdc.0, pool_information.usdc.1 )
}
#[test]
- fn [](
+ fn [](
) {
let env = ScryptoUnitEnv::new();
let pool_information = mainnet_state::pool_information(&env.resources);
@@ -563,7 +563,7 @@ macro_rules! define_price_test {
)*
$(
#[test]
- fn [](
+ fn [](
) {
let env = ScryptoUnitEnv::new();
let pool_information = mainnet_state::pool_information(&env.resources);
@@ -576,7 +576,7 @@ macro_rules! define_price_test {
test_effect_of_price_action_on_fees($multiplier, env, pool_information.bitcoin.0, pool_information.bitcoin.1 )
}
#[test]
- fn [](
+ fn [](
) {
let env = ScryptoUnitEnv::new();
let pool_information = mainnet_state::pool_information(&env.resources);
@@ -589,7 +589,7 @@ macro_rules! define_price_test {
test_effect_of_price_action_on_fees($multiplier, env, pool_information.ethereum.0, pool_information.ethereum.1 )
}
#[test]
- fn [](
+ fn [](
) {
let env = ScryptoUnitEnv::new();
let pool_information = mainnet_state::pool_information(&env.resources);
@@ -602,7 +602,7 @@ macro_rules! define_price_test {
test_effect_of_price_action_on_fees($multiplier, env, pool_information.usdc.0, pool_information.usdc.1 )
}
#[test]
- fn [](
+ fn [](
) {
let env = ScryptoUnitEnv::new();
let pool_information = mainnet_state::pool_information(&env.resources);
@@ -930,7 +930,9 @@ fn test_effect_of_price_action_on_fees(
.build(),
&private_key,
);
- receipt.expect_commit_success();
+ if !receipt.is_commit_success() {
+ return;
+ }
println!(
"Open - Multiplier = {}x, Bin Span = {}, Cost = {} XRD, Execution Cost = {} XRD",
multiplier,