This repository hosts an implementation of the Rebalancing Automated Market Maker, or RAMM, in Sui Move. Here is the whitepaper describing the RAMM: https://aldrin.com/RAMM-whitepaper.pdf
At present, the repository contains the following:
- 2 Sui Move packages:
ramm-suicontains an implementation for the RAMM, which is ongoing work.ramm-mischas a faucet with tokens useful for testnet development/testing- this Move library also has a simple demo that showcases price information querying from Switchboard aggregators
- a Rust crate that uses the Sui Rust SDK to automate the RAMM deployment process
- RAMM in Sui Move
- Deploying and testing the RAMM on the testnet
- Deployment automation tool
- Testing a Switchboard price feed
- Regarding AMMs with variable numbers of assets in Sui Move
The principal data structure in the ramm-sui package is the RAMM object.
In order for traders to
- deposit/withdraw liquidity
- buy/sell assets
they must interact with a
RAMMobject through the contract APIs in theinterface*modules.
The public API, in ramm_sui/sources, is split in different modules:
- Functions that can be called on RAMMs of any size exist in
ramm_sui::ramm - for 2-asset RAMMs, the module
ramm_sui::interface2is to be used - for 3-asset RAMMs, use
ramm_sui::interface3- any future additions of higher-order RAMMs will follow this pattern: 4-asset RAMMs =>
ramm_sui::interface4, etc.
- any future additions of higher-order RAMMs will follow this pattern: 4-asset RAMMs =>
- mathematical operators related to the RAMM protocol in
ramm_sui::math
The ramm_sui/tests/ directory has an extensive suite of tests for the RAMM's functionality.
Among them:
- Utilities used to create non-trivial test scenarios, and avoid boilerplate when setting up test
environments in
test_util.move - Tests to the basic mathematical operators required to implement the RAMM, in
math_tests.move - Basic unit-tests for RAMM creation and initialization, in
ramm_tests.move - Safety tests for each of the sized RAMM's interfaces:
interface2_safety_tests.movefor 2-asset RAMMs, and so on; these safety tests include:- checking that priviledged RAMM operations performed with an incorrect
Capobject fail - providing an incorrect
Aggregatorto trading functions will promptly fail
- checking that priviledged RAMM operations performed with an incorrect
- End-to-end tests in
interface{n}_tests.movethat use the functionality present insui::test_scenarioto flow from RAMM andAggregatorcreation, all the way to liquidity deposits, withdrawals and trading with the RAMM - Tests to the RAMM's volatility fee in
volatility{n}_tests.move
The structure stores information required for its management and operation, including datum about each of its assets:
- the
AdminCaprequired to perform gated operations e.g. fee collection; see here for more information - minimum trade amounts per each asset
- the balance of each asset (in a scalar and typed version, see below)
- a data structure specific to Sui (
balance::Supply) that regulates LP token issuance for each asset - protocol fees collected for each asset
Some RAMM operations don't require administrative privileges - trading, liquidity deposits/withdrawals - while others must. Examples:
- add assets to an unitialized RAMM
- initialize a RAMM, thereby freezing the number and type of its assets
- disable/enable deposits for an asset
- transfer collected protocol fees to a designated address
- change the designated fee collection address
- change the minimum trading amount for an asset
In order to do this, Sui Move allows the use of the capability pattern.
Upon creation, each RAMM will have 2 associated Capability objects that will be owned by whoever created the RAMM:
- A perennial
RAMMAdminCap, required in every gated operation. ItsIDwill be stored in the RAMM, and checked so that only the correct object unlocks the operation - An ephemeral
RAMMNewAssetCap, whoseIDis also stored in the RAMM, in anew_asset_cap_id: Option<ID>field. The reason why it's ephemeral:- This
Capis used to add assets to the RAMM, which can only be done before initialized - To initialize a RAMM, its
RAMMNewAssetCapmust be passed by value to be destroyed, and thenew_asset_cap_idfield becomesOption::Noneto mark its initialization - After initialization, no more assets can then be added, which is enforced by the fact that the
RAMMNewAssetCapno longer exists
- This
There are some limitations to the chosen RAMM design.
Because of limitations with Sui Move's type system, in order to both
- create RAMMs with arbitrary asset counts, and
- abstract over asset types
and still have a degree of code reuse, it is necessary to store certain information twice:
- once in an untyped, scalar format e.g.
u256, and - again in a typed format, e.g.
Balance<Asset>.
This information is:
- per-asset balance information
- per-asset LP token
Supplystructures, which regulate LP token issuance
Doing this decouples the internal RAMM functions, which can do the calculations required for trading and liquidity operations
using scalars only - see ramm_sui::ramm::{trade_i, trade_o} - from the client-facing public API, that must have access
to the asset types themselves, and their count - see ramm_sui::interface2::trade_amount_in_2 and
ramm_sui::interface3::trade_amount_in_3.
In other words, the only code that must be repeated for every class of RAMMs is the public, typed API, every instance of which will wrap the same typeless, scalar internal functions.
In order for orders to be sent to the RAMM and affect its internal state, it must be shared, and thus cannot have an owner.
This makes it subject to consensus in the Sui network, preventing traders' orders from benefitting from the fast-tracking of transactions that occurs in contexts of in sole object ownership and object immutability.
In order to obtain current information on asset pricing, the RAMM requires the use of oracles. In Sui, at present, there are only two alternatives: Pyth Network, and Switchboard.
Switchboard was chosen over Pyth due to its simplicity - Pyth requires attested off-chain data to be provided in each price request, while Switchboard does not.
However, unlike in the EVM where the RAMM could store each oracle's address to then interact with,
Sui's object model prevents interaction via an address alone, and as such:
Switchboard's
Aggregators cannot be stored in the RAMM object.
This is because the RAMM must be a (shared) object; whereby it must have the key ability.
If RAMM has key, then
- all its fields must have
store - in particular,
vector<Aggregator>must have store- so
Aggregatormust havestore
- so
- Which it does not, so RAMM cannot have
key - Meaning it cannot be used be turned into a shared object with
sui::transfer::share_object - which it must be, to be readable and writable by all
Every trading/liquidity provision function in the public interface requires an Aggregator to be
passed as an argument, so that the RAMM can fetch each assets' most recent pricing data with which
to run protocol operations.
It is possible that an issue with Switchboard (or further upstream) causes an asset's pricing data
to not be updated, leading its Aggregator, in turn, to return stale prices for that asset.
In order to prevent these stale prices from being used by the RAMM, the public interface must be
supplied with the address of the Sui network's global clock, with type sui::clock::Clock.
As said in the official documentation:
An instance of Clock is provided at address
0x6, no new instances can be created.
As of January 2024, Mysten Labs has sunset Sui Move support for the MSL and the Move Prover; see:
Thus, the below notes are no longer accurate. They are kept for future reference, should such support return in the future.
Sui Move (as well as other variants of Move) supports the usage of the Move Prover to formally verify the behavior of Move programs that have been annotated with sentences in MSL, or Move Specification Language, a subset of Move.
The ramm-sui library leverages, where possible, the MSL and the Move Prover to verify parts of
its code, taking into account at least 2 limitations:
- An AMM is a complex object whose behavior can be difficult, or even impossible to fully specify in MSL
- Mysten Labs is currently offering only limited support to the integration of the Move Prover
with the Sui dialect of Move; see: MystenLabs/sui#14348
- With planned future editions of Sui Move evolving the language further, this situation is unlikely to change, as MP/MSL were originally developed for the Diem/Libra dialects of Move; see MystenLabs/sui#14062
With the above caveats, here's how to leverage the prover to verify annotated functions from
ramm-sui.
# Assume one is at the root of the repository
cd ramm-sui
# Select a function for which a `spec` has been written
sui move prove --path . -- --verify-only transfer_admin_capIt is currently not possible to simply run
cd ramm-sui
sui move prove --path .over the whole Move package due to the prover timing out.
Furthermore, although there is a flag to run the prover on a single module,
cd ramm-sui
sui move prove --path ~/Work/aldrin/ramm-sui/ramm-sui/ --target rammit is currently malfunctioning.
The Bash variables below should be declared in a terminal/script for ease of use when running the example commands.
The latest package IDs of
- the
ramm_suipackage, which is the library to create/interact with RAMM objects, as well as the ramm_miscpackage, used to create test tokens on the testnet,
are the following:
export FAUCET_PACKAGE_ID=0x76a5ecf30b2cf49a342a9bd74a479702a1b321b0d45f06920618dbe7c2da52b1 \
export RAMM_SUI_PACKAGE_ID=0x0adad52b9aa0a00460e47c3d5884dd4610bafdd772d62321558005387abe1174The object IDs of
- the most recently created
ramm_misc::faucet::Faucetobject, as well as - a 3-asset
BTC/ETH/SOLRAMM, and- its fee collection address (can be changed)
- its admin capability, and
- its new asset capability (since deleted with its initialization)
are:
export FAUCET_ID=0xaf774e31764afcf13761111b662892d12d6998032691160e1b3f7d7f0ab039bd \
export RAMM_ID=0xbee296f4efc42bb228c284c944d58c28a971d5c29c015ba9fe6b0db20b07896d \
export FEE_COLLECTOR=0x1fad963ac9311c5f99685bc430dc022a5b0d36f6860603495ca0a0e3a46dd120 \
export ADMIN_CAP_ID=0xaacbaebf49380e6b5587ce0a26dc54dc4576045ff9c6e3a8aab30e2b48e81ecd \
export NEW_ASSET_CAP_ID=0xb7bcf12b4984e0ea6b11a969b4bc2fa11efa3d488b6ba6696c43425c886d2915The object IDs of
- a 2-asset
ETH/USDCRAMM, and- its fee collection address (can be changed)
- its admin capability, and
- its new asset capability (since deleted with its initialization)
are
export RAMM_ID=0x14cd5b0a0fdb09ca16959ed8b30ac674521fed8ed0089ff4a3d321f3295668ef \
export FEE_COLLECTOR=0x1fad963ac9311c5f99685bc430dc022a5b0d36f6860603495ca0a0e3a46dd120 \
export ADMIN_CAP_ID=0x0c4baabcfe4b9fcfe7c45c5bf5f639e54ab948be0794d8cc9246545edcb8f49a \
export NEW_ASSET_CAP_ID=0xf3d8e8f21e84d4220cec2edb1e30bb3667a57d390d6298e68bbeef2b202e105eVerify these using tsui client object {object-id}.
The object IDs of the six Switchboard Aggregators presently on the Sui testnet, for
BTC, ETH, SOL, SUI, USDT, USDCare:
export BTC_AGG_ID=0x7c30e48db7dfd6a2301795be6cb99d00c87782e2547cf0c63869de244cfc7e47 \
export ETH_AGG_ID=0x68ed81c5dd07d12c629e5cdad291ca004a5cd3708d5659cb0b6bfe983e14778c \
export SOL_AGG_ID=0x35c7c241fa2d9c12cd2e3bcfa7d77192a58fd94e9d6f482465d5e3c8d91b4b43 \
export SUI_AGG_ID=0x84d2b7e435d6e6a5b137bf6f78f34b2c5515ae61cd8591d5ff6cd121a21aa6b7 \
export USDT_AGG_ID=0xe8a09db813c07b0a30c9026b3ff7d5617d2505a097f1a90a06a941d34bee9585 \
export USDC_AGG_ID=0xde58993e6aabe1248a9956557ba744cb930b61437f94556d0380b87913d5ef47Suibase is a tool that assists in the development, testing and deployment of Sui smart contracts.
It provides a suite of tools and SDKs for Rust/Python that let developers easily target
different Sui networks (e.g. devnet, testnet, main) and configure the development environment,
e.g. by allowing the specification of an exact version of the sui binaries, and/or from a forked
repository.
For the purposes of this project, suibase will be needed to build/test/deploy the RAMM on a given
network - in this case, the testnet.
After installing suibase, optionally setting
the sui version to be used, and running testnet start, tsui will be ready for use in the
user's $PATH.
In order to create/interact with the RAMM, fictitious tokens are required.
Then, for the purpose of creating test coins to be used to interact with the RAMM,
ramm-misc/sources/test_coins offers 5 different tokens for which there exists a corresponding
Switchboard Aggregator on the Sui testnet:
BTC, ETH, SOL, USDT, USDC
SUI for gas fees can be requested in the Sui Discord server.
To interact with the faucet, the following data are required:
- The ID of the
ramm_miscpackage, which contains the faucet: it may beexported asFAUCET_PACKAGE_ID- See above a list of currently published package IDs
- The ID of a
ramm_misc::faucet::Faucetthat is currently instantiated on the testnet, may it be calledFAUCET_ID- See above
- The amount of the token to be minted,
COIN_AMNT.- See below for a note on how many decimal places each fictitious asset has
- A type argument specifying the specific asset to be requested,
exported asCOIN- In the case of e.g.
ETH, it'll be"$FAUCET_PACKAGE_ID"::test_coins::"$COIN, which will expand to"$FAUCET_PACKAGE_ID"::test_coins::ETH
- In the case of e.g.
After these data are set as variables, a specific token, i.e. ramm_misc::test_coins::BTC, can be
requested with
tsui client call --package "$FAUCET_PACKAGE_ID" \
--module test_coin_faucet \
--function mint_test_coins \
--args "$FAUCET_ID" "$COIN_AMNT" \
--type-args "$FAUCET_PACKAGE_ID"::test_coins::"$COIN" \
--gas-budget 100000000For these tests, all assets can be considered to have 8 decimal places.
However, when using real tokens bridged from other chains into Sui, in order to obtain its decimal place count, do the following:
- Obtain the ID of the package containing the Sui-side version of the bridged token from
the official docs; in the case of
WSOL, it isexport PACKAGE_ID=0xb7844e289a8410e50fb3ca48d69eb9cf29e27d223ef90353fe1bd8e27ff8f3f8 - Use a Sui RPC inspector to run the
get_CoinMetadatamethod onPACKAGE_ID::coin::COIN - In the JSON response, the
decimalsfield will be the decimal places the token was configured with; forWSOL,8
In order to create a RAMM, the following data are necessary:
- The previously stored
RAMM_PACKAGE_ID, and - an address for fee collection needs to be specified, e.g. as
FEE_COLLECTOR.
See above for the addresses of currently published RAMMs.
After the above:
tsui client call --package "$RAMM_PACKAGE_ID" \
--module ramm \
--function new_ramm \
--args "$FEE_COLLECTOR" \
--gas-budget 1000000000The previous transaction should have resulted in several newly created objects:
- the RAMM object, which should be
exported asRAMM_ID - an admin capability object,
ADMIN_CAP_ID - a capability used to add new assets,
NEW_ASSET_CAP_ID
For an asset to be added, assuming its Aggregator's ID from
the list of presently available testnet aggregators
has been exported as AGGREGATOR_ID:
tsui client call --package "$RAMM_PACKAGE_ID" \
--module ramm \
--function add_asset_to_ramm \
--args "$RAMM_ID" "$AGGREGATOR_ID" $MIN_TRADE_AMNT $ASSET_DEC_PLACES "$ADMIN_CAP_ID" "$NEW_ASSET_CAP_ID" \
--gas-budget 1000000000The values MIN_TRADE_AMNT and ASSET_DEC_PLACES are the asset's minimum trade amount, and
its decimal places, respectively.
See the note above to know how many decimal places
each test token has.
Run the following
tsui client call --package "$RAMM_PACKAGE_ID" \
--module ramm \
--function initialize_ramm \
--args "$RAMM_ID" "$ADMIN_CAP_ID" "$NEW_ASSET_CAP_ID" \
--gas-budget 1000000000This will delete the new asset capability associated with this RAMM whose ID is NEW_ASSET_CAP_ID,
so no more assets can be added to that RAMM.
Carefully consider the RAMM's desired asset count before initializing it.
Consider a concrete example of a BTC/ETH/SOL 3-asset RAMM.
As the RAMM has 3 assets, the corresponding public interface must be used.
In order to deposit liquidity for an asset in the RAMM, the following data are required:
- The previously stored
$RAMM_ID - The coins previously requested from the faucet
- in this case,
$BTC_IDis the object ID of theCoin<$FAUCET_PACKAGE_ID::test_coins::BTC>gotten from the faucet
- in this case,
- Aggregator IDs for each of the RAMM's 3 assets, once again gotten from here
$BTC_AGG_IDforBTC$ETH_AGG_IDforETH, etc
- the type information of each of the RAMM's assets
- in this case,
$FAUCET_PACKAGE_ID::test_coins::BTCforBTC $FAUCET_PACKAGE_ID::test_coins::ETHforETH, etc
- in this case,
Note that:
- the first type provided corresponds to the type of the coin object i.e. of the asset for which liquidity is being deposited
- the order in which the
Aggregators are provided must match the order in which the types are given
All of the above results in the following:
tsui client call --package "$RAMM_PACKAGE_ID" \
--module interface3 \
--function liquidity_deposit_3 \
--args "$RAMM_ID" 0x6 "$BTC_ID" "$BTC_AGG_ID" "$ETH_AGG_ID" "$SOL_AGG_ID" \
--gas-budget 1000000000 \
--type-args "$FAUCET_PACKAGE_ID::test_coins::BTC" "$FAUCET_PACKAGE_ID::test_coins::ETH" "$FAUCET_PACKAGE_ID::test_coins::SOL" The examples below assume a 3-asset BTC/ETH/SOL RAMM with existing initial liquidity.
In order to eg. deposit exactly 20 ETH into the RAMM, the following data are required:
$RAMM_ID- The ID of the
Coin<$FAUCET_PACKAGE_ID::test_coins::ETH>previously requested from the faucet - The minimum amount of the outbound asset the trader expects to receive, which can be
exported asMIN_AMNT_OUT.- Recall that all test coins are created to have 8 decimal places, so e.g. 1 unit of
BTCshould be100000000
- Recall that all test coins are created to have 8 decimal places, so e.g. 1 unit of
- Aggregator IDs for each of the RAMM's 3 assets, as always taken from here
- the type information of each of the RAMM's assets
- in this case,
$FAUCET_PACKAGE_ID::test_coins::BTCforBTC, etc
- in this case,
Note that:
- the first type provided corresponds to the inbound asset, as well as the type of the coin object
- the second type provided corresponds to outbound asset
- the order in which the
Aggregators are provided must match the order in which the types are given
tsui client call --package "$RAMM_PACKAGE_ID" \
--module interface3 \
--function trade_amount_in_3 \
--args "$RAMM_ID" 0x6 "$ETH_ID" "$MIN_AMNT_OUT" "$ETH_AGG_ID" "$BTC_AGG_ID" "$SOL_AGG_ID" \
--gas-budget 1000000000 \
--type-args "$FAUCET_PACKAGE_ID::test_coins::ETH" "$FAUCET_PACKAGE_ID::test_coins::BTC" "$FAUCET_PACKAGE_ID::test_coins::SOL"In order to e.g. withdraw exactly 1 BTC from the RAMM, the following data are required:
$RAMM_ID- The amount of the outbound asset the trader wishes to receive, which can be
exported asAMNT_OUT.- Recall that all test coins are created to have 8 decimal places, so e.g. 1 unit of
BTCshould be100000000
- Recall that all test coins are created to have 8 decimal places, so e.g. 1 unit of
- The ID of the coin object previously requested from the faucet
- Aggregator IDs for each of the RAMM's 3 assets, as always taken from here
- the type information of each of the RAMM's assets
- in this case,
$FAUCET_PACKAGE_ID::test_coins::BTCforBTC, etc
- in this case,
Note that:
- the first type provided corresponds to the inbound asset, as well as the type of the coin object
- the second type provided corresponds to outbound asset
- the order in which the
Aggregators are provided must match the order in which the types are given
tsui client call --package "$RAMM_PACKAGE_ID" \
--module interface3 \
--function trade_amount_out_3 \
--args "$RAMM_ID" 0x6 "$AMNT_OUT" "$BTC_ID" "$BTC_AGG_ID" "$ETH_AGG_ID" "$SOL_AGG_ID" \
--gas-budget 1000000000 \
--type-args "$FAUCET_PACKAGE_ID::test_coins::BTC" "$FAUCET_PACKAGE_ID::test_coins::ETH" "$FAUCET_PACKAGE_ID::test_coins::SOL"Below are the data required to perform a liquidity withdrawal from the RAMM.
This example also considers the above 3-asset BTC/ETH/SOL pool.
- The
$RAMM_IDis necessary - The object ID, call it
$LP_IDof the liquidity pool (LP)Coins emitted by the pool upon the asset's prior deposit - Aggregator IDs for each of the RAMM's 3 assets, as always taken from here
- the type information of each of the RAMM's assets, appended by the type of the asset meant to be
withdrawn
- in this case, since the pool has 3 assets, 4 type arguments are needed
Note that:
- the last type provided corresponds to
- the inbound LP tokens which will be burned
- the outbound asset
- the order in which the
Aggregators are provided must match the order in which the pool's types are given
tsui client call --package "$RAMM_PACKAGE_ID" \
--module interface3 \
--function liquidity_withdrawal_3 \
--args "$RAMM_ID" 0x6 "$LP_ID" "$BTC_AGG_ID" "$ETH_AGG_ID" "$SOL_AGG_ID" \
--gas-budget 1000000000 \
--type-args "$FAUCET_PACKAGE_ID::test_coins::BTC" "$FAUCET_PACKAGE_ID::test_coins::ETH" \
"$FAUCET_PACKAGE_ID::test_coins::SOL" "$FAUCET_PACKAGE_ID::test_coins::BTC"In order to automate part of the process described in the previous section,
the ramm-sui-deploy Rust crate leverages the Sui Rust SDK to allow a user to
- create a TOML file specifying
1.1. the target network (i.e. Sui testnet or mainnet)
1.2. whether to publish a new version of
ramm-sui, or to use an existing version 1.3. its asset count and fee collection address 1.4. its assets and their data - create a RAMM with the specified parameters
- add the specified assets to it
- initialize it
In deployment_cfgs/example.toml is an example TOML config for a 3-asset RAMM using assets from a
published version of ramm-misc.
Assuming suibase is installed, and its workdir for the intended network has been initialized
- see https://suibase.io/how-to/devnet-testnet.html - run the following command to deploy and initialize a RAMM:
cd ./ramm-sui-deploy
# keep in mind the location of `deploy_cfg.toml` relative to ./ramm-sui-deploy
cargo run --bin ramm_sui_deploy -- --toml ../deployment_cfgs/testnet/deploy_cfg.tomlA list of price information feeds currently available on the test Sui testnet can be found here.
In order to test a price feed from the CLI using the ramm_misc package, perform the following
actions:
cd ramm-misc
sui move build
sui client publish --gas-budget 20000000
# export the above package ID to $FAUCET_PACKAGE_ID
# $AGGREGATOR_ID is an ID from https://app.switchboard.xyz/sui/testnet
sui client call \
--package $FAUCET_PACKAGE_ID \
--module switchboard_feed_parser \
--function log_aggregator_info \
--args $AGGREGATOR_ID \
--gas-budget 10000000 \
# export the resulting object ID to AGGREGATOR_INFO
sui client object $AGGREGATOR_INFOThe relevant information will be in the latest_result, latest_result_scaling_factor
fields.
Note that a Switchboard Aggregator's price timestamp is in the Unix format, although
measured in seconds, and not in milliseconds as is the case with e.g. Sui's global clock,
at address 0x6.
As of its release, Sui Move will not allow a single implementation of a RAMM for varying numbers of assets.
In other words, in order to have a RAMM with 3 assets, this will require
one implementation for that number of assets.
In order to support a RAMM with 4 assets, there will need to be another, and so forth.
To illustrate this, in ramm-misc/tests/coin_bag.move there's a short example using the
testing tokens previously defined.
In ramm-misc/tests/coin_bag.move module, there is a test which creates several different kinds
of testing tokens, and inserts them in a sui::bag::Bag.
The goal of this experiment is to perform a trivial operation: add all of the testing tokens' amounts. Doing this requires fully instantiating the types of all the involved tokens, which by consequence prevents a fully generic RAMM from being implemented in Sui Move (as of its release). The reasoning supporting this conclusion will be below.
Skip further below for the conclusion.
- A
Bagwas chosen since it andObjectBagare the only heterogeneous collections in the Sui Move standard library. - By holding tokens of different types in the
Bag, the RAMM's state requirements are simulated in a simplified approximation. In the whitepaper's description of a RAMM, for a pool withnassets, its internal state includes:
- the balances
B_1, B_2, ..., B_n, which in Sui Move are necessarily typed objects: both theCoin<T>objects, and their internal structureBalance<T>require typed information to be used e.g. to receive a user's trade for/against a certain asset - the liquidity pool's tokens for each asset. Regardless of the way they are represented
internally, since the RAMM protocol requires the ability to mint/burn LP tokens when
users add/remove liquidity, this will, per Sui Move's restrictions, involve the use
of a
TreasuryCap<T>, which is also a typed structure.
As mentioned above, in ramm-misc/sources/coin_bag is a module consisting of a simple test:
adding the amount of several tokens, all of different types.
This test is simple:
- Tokens from
ramm-misc/sources/test_coinsare minted usingsui::coin::mint_for_testing
let amount: u64 = 1000;
let btc = coin::mint_for_testing<BTC>(amount, ctx);
let sol = coin::mint_for_testing<SOL>(amount * 2, ctx);
...- A bag is initialized:
use sui::bag;
let bag = bag::new(...);- Those tokens are inserted into the bag using
u64keys:
bag::add<u64, Coin<BTC>>(&mut bag, 0, btc);
bag::add<u64, Coin<SOL>>(&mut bag, 1, sol);
...Although the bag insertion function has signature
public fun add<K: copy + drop + store, V: store>(bag: &mut Bag, k: K, v: V),
inserting values of different types into the collection is not the issue; in the case of
a hypothetical structure
struct RAMM {
...
// Hypothetical map between `T` and `Coin<T>`
balances: Bag,
// Hypothetical map between `T` and `Coin<LPToken<T>>`
lpt_balances: Bag,
...
}
public fun add_asset_to_pool<T>(&mut self: RAMM) {
...
self.balances[T] = 0;
self.lpt_balances = 0;
...
}then the pool's internal state could be created incrementally, binding the above K, V to
a different asset, one asset at a time.
The issue arises when accessing the values.
let btc = bag::remove<u64, Coin<BTC>>(&mut bag, 0);
amnt = amnt + coin::burn_for_testing(btc);
let sol = bag::remove<u64, Coin<SOL>>(&mut bag, 1);
amnt = amnt + coin::burn_for_testing(sol);
let eth = bag::remove<u64, Coin<ETH>>(&mut bag, 2);
amnt = amnt + coin::burn_for_testing(eth);
let usdc = bag::remove<u64, Coin<USDC>>(&mut bag, 3);
amnt = amnt + coin::burn_for_testing(usdc);All of the functions that access a bag's values take a V type parameter,
in this case, remove:
public fun remove<K: copy + drop + store, V: store>(bag: &mut Bag, k: K): VThe bag in question has 4 types of tokens, so in order to access all of its elements,
remove has to be instantiated with exactly 4 unique types.
In other words - it is not possible to sum all of the token amounts in the bag
without enumerating all of the types in it - which is a variable number.
It can be seen that for a Bag - or any other collection - with N different
types in it, where N is known only at runtime, the number of unique instantiations would be
N too.
Herein is the problem:
- functions in Sui Move cannot be variadic in their type parameters
- i.e.
fun f<T, U>is different fromfun f<T, U, V>, and the first cannot be called with less or more than 2 parameters, as Sui Move requires all generic types and struct to be fully instantiated at runtime
- i.e.
- having a pool with a variable number of assets would require functions with variable number of type parameters, which is not possible in Sui Move
- In Sui Move, it is not possible to build structures with a dynamic number of type parameters
- Each class of RAMM protocols - 2 assets, 3, 4, and so on - will require its own code,
- some of this code will need to be replicated for each pool size
This project is distributed under AGPL-3.0-only.