diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..60fc97f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Rome Protocol + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 62d7fff..97eb8cf 100644 --- a/README.md +++ b/README.md @@ -2,122 +2,56 @@ > **Built on [Rome Protocol](https://docs.rome.builders)** — EVM chains that run natively inside the Solana runtime, where Solidity apps call Solana programs atomically (CPI) and Solana users drive EVM apps: two VMs, one chain, one block. -`rome-solidity` is a Solidity smart contract monorepo for **bidirectional EVM ↔ Solana interop primitives** within the Rome-EVM program stack, token utilities, and a Meteora DEX AMM implementation, tested via Hardhat. +`rome-solidity` is the **Solidity SDK for Rome** — the interfaces, wrappers, and contracts a Solidity builder uses to reach Solana from an EVM contract: call any Solana program (CPI), treat any SPL token as an ERC-20, bridge assets out of Rome, and read Solana price feeds through the standard Chainlink interface. -## Key goals +**Full reference:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — every contract, what it's for, and where it's used. -- Solana-compatible token/account behavior in EVM style -- `Meteora DAMMv1`: automated market maker + factory/pool system -- **Cross-program invocation wrappers (CPI)** — let Solidity contracts invoke native Solana programs via the precompile at `0xff…08` (plus gas-token wrap/unwrap, withdraw) -- **Surface for MetaHook callbacks** — Solana programs invoke Rome EVM contracts with a synthetic EVM sender derived from the calling Solana program ID; Solidity contracts can target this surface to be invoked from Solana side -- ERC20 interface to SPL tokens (supports liquidity flow in both directions: Solana liquidity consumed in Solidity, Rome-side liquidity contributed back to Solana protocols) +## What's inside — ordered by importance ---- +1. **Precompile interfaces** — [`contracts/interface.sol`](contracts/interface.sol). The ABI bindings to Rome's non-EVM precompiles: **CPI** (`0xff…08`), **Helper** (`0xff…09`), **System** (`0xff…07`), **Withdraw** (`0x42…16`), and the gas-optimised **cached** family (`0xff…04/05/06/0b`). Everything else builds on these. +2. **CPI toolkit** — [`contracts/cpi/`](contracts/cpi/). Call *any* Solana program from Solidity — account-meta builders, Anchor encoding, PDA derivation, cost quoting, and adapter templates. The differentiator. (Guide: [`contracts/cpi/README.md`](contracts/cpi/README.md).) +3. **SPL ↔ ERC-20 wrappers** — [`contracts/erc20spl/`](contracts/erc20spl/). Any SPL token *is* an ERC-20 on Rome. Two tracks (**cached** vs **CPI** — see below) plus `ERC20SPLFactory` to wrap existing mints or mint new SPL tokens. +4. **Bridge** — [`contracts/bridge/`](contracts/bridge/). The on-chain egress from Rome: `RomeBridgeWithdraw` (five rails) over Circle **CCTP v2** and **Wormhole**. (The off-chain orchestrator is the separate `rome-bridge-api`.) +5. **Oracle** — [`contracts/oracle/`](contracts/oracle/). Solana price feeds (Pyth, Switchboard) delivered to EVM contracts through the standard Chainlink `AggregatorV3Interface`; direct + cached adapters, a clone factory, and a batch reader. +6. **Account & token primitives** — `rome_evm_account.sol` (PDA derivation), `activation/SimpleActivator.sol` (one-tx user-paid activation), `wrap/WrappedGasFacade.sol`, and the `spl_token/` · `system_program/` · `convert.sol` low-level libraries. +7. **Examples** — [`contracts/examples/`](contracts/examples/). Worked references for the toolkit. -## Quick start - -### Requirements - -- Node.js 18+ (recommended) -- npm / yarn -- Hardhat dependencies (installed by `npm install` below) - -### Install - -```bash -npm install -``` - -### Compile +## The two SPL wrapper tracks — which to use -```bash -npx hardhat compile -``` - ---- - -# Meteora DAMMv1 integration +Both `SPL_ERC20_cached` and `SPL_ERC20` expose the identical `IERC20` surface over the same SPL mint. A contract uses **one track, never both** (a hard rule). -This integration can be used as an example of how Rome-EVM can provide interoperability with native Solana smart-contracts. +| Use **`SPL_ERC20_cached`** (the default) | Use **`SPL_ERC20`** (CPI-based) | +|---|---| +| Standard ERC-20 flows; multi-step / iterative-VM composition (DEX multi-hop, bulkers); anything needing the Solana side effect to **revert atomically** with the EVM tx. Cheaper CU. The factory deploys this. | Only when you need to push SPL out to an **arbitrary raw Solana wallet** (`bridgeOutToSolana` / `ensureRecipientAta`), which requires the permanently-CPI-only `create_ata_for_key`. | -## Preparation +Full explanation: [`docs/ARCHITECTURE.md` §3](docs/ARCHITECTURE.md#3-spl--erc-20-wrappers--contractserc20spl). -1. Go to https://devnet.meteora.ag/pools#dynamicpools and find Meteora **DAMMv1** pool which your would like to use from within Rome-EVM -2. Copy the address of this pool (base58) and convert it into HEX. Later we will refer to this address as *POOL_ADDRESS* +## Import paths -## Deployment - -Example scripts: -- deploy_meteora_factory.ts -- deploy_meteora_pool.ts - -```bash -export _PRIVATE_KEY= -export POOL_ADDRESS= -npx hardhat run scripts/deploy_meteora_factory.ts --network -npx hardhat run scripts/deploy_meteora_pool.ts --network +```solidity +import {ICrossProgramInvocation, IHelperProgram, CpiProgram, HelperProgram} + from "@rome-protocol/rome-solidity/contracts/interface.sol"; +import {SPL_ERC20_cached} from "@rome-protocol/rome-solidity/contracts/erc20spl/erc20spl_cached.sol"; +import {IAggregatorV3Interface} from "@rome-protocol/rome-solidity/contracts/oracle/IAggregatorV3Interface.sol"; ``` -After successful deployment, you will see a new file `/deployments/.json` -which contains information about deployed smart contracts. This file is later used by tests. - ---- +npm publish is pending — consume via a github-pinned git dependency or by copying files. (The CPI-precompile ABIs are also mirrored in [`@rome-protocol/sdk`](https://github.com/rome-protocol/rome-sdk-ts) for TypeScript.) -## Tests - -Set tester private key in dev keystore: - -```bash -npx hardhat keystore set _PRIVATE_KEY --dev -``` - -Use Hardhat test suite: +## Quick start ```bash -npx hardhat test tests/damm_v1_pool.integration.ts --network local +npm install +npx hardhat compile +# network-independent unit tests (oracle + bridge + CPI foundation): +npx hardhat test nodejs tests/oracle/*.test.ts tests/bridge/*.test.ts tests/cpi/*.test.ts ``` -## Contract highlights - -- `MeteoraDAMMv1Factory` - - create pool factory - - manage pool creation and fee config -- `DAMMv1Pool` - - liquidity add/remove (WIP) - - swap with invariant (WIP) - - fee model -- `ERC20SPLFactory` + `SPL_ERC20` - - minting/burning wrapped SPL tokens - - bridging SPL tokens to ERC20 style +Solidity `0.8.28`. Integration tests under `tests/**/*.integration.ts` require a live Rome chain (`--network local`, or a devnet/testnet chain); see [`hardhat.config.ts`](hardhat.config.ts) for configured networks. ---- - -## Project policies - -- Solidity currently targeted: 0.8.28 -- Prefer non-reentrant checks and SafeMath semantics (built in) -- Maintain artifacts + `build-info` for deterministic testing -- Cross-program invocations encouraged through clean wrappers in `interface.sol` - ---- - -## Contributing - -1. Fork repo -2. branch `feature/` -3. add/adjust tests -4. `npm test` -5. PR with explanation + gas/security notes - ---- - -## Notes +## Building on Rome with an agent -- artifacts includes compiled JSON from existing snapshot builds. -- `deployments/.json` carries the deployed contract metadata for each chain (per `hardhat.config.ts` networks). See `hardhat.config.ts` for the configured networks. -- Keep toolchain with hardhat.config.ts and `tsconfig` consistent with existing pattern. +See [`AGENTS.md`](AGENTS.md) — the Rome-specific rules a coding agent needs — and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full contract map. ---- +## License -## Building on Rome with an agent -See [`AGENTS.md`](./AGENTS.md) — the Rome-specific rules a coding agent needs. +MIT — see [`LICENSE`](LICENSE). diff --git a/contracts/cpi/test/PdasBatchWrapper.sol b/contracts/cpi/test/PdasBatchWrapper.sol index 53c198b..1b23942 100644 --- a/contracts/cpi/test/PdasBatchWrapper.sol +++ b/contracts/cpi/test/PdasBatchWrapper.sol @@ -81,8 +81,8 @@ contract PdasBatchWrapper { bumpB = rB.bump; } - /// Live-precompile path: 5-PDA generic batch (mirrors Meteora's - /// dynamic_amm_program group shape used by `damm_v1_pool.sol`). + /// Live-precompile path: 5-PDA generic batch (a typical + /// multi-PDA adapter group shape). function deriveFive( bytes32 pool, bytes32 token_a_mint, diff --git a/contracts/examples/pdas_batch.sol b/contracts/examples/pdas_batch.sol index a565504..0b5abd2 100644 --- a/contracts/examples/pdas_batch.sol +++ b/contracts/examples/pdas_batch.sol @@ -10,9 +10,8 @@ import {PdasBatch} from "../cpi/PdasBatch.sol"; /// derivation on the CPI precompile (`0xFF…08`, selector /// `0x944336f8`). /// -/// The example mirrors the simplified Meteora "five amm-program -/// PDAs in one syscall" shape that ships in `damm_v1_pool.sol` -/// (PR D of the PdasBatch surface rollout). +/// The example mirrors a common "five program PDAs in one +/// syscall" adapter shape. /// /// Read this when you need to derive 2+ PDAs against the same /// Solana program in one call. For 1 PDA, prefer `PdaDeriver`. diff --git a/contracts/meteora/damm_v1_factory.sol b/contracts/meteora/damm_v1_factory.sol deleted file mode 100644 index 395942d..0000000 --- a/contracts/meteora/damm_v1_factory.sol +++ /dev/null @@ -1,251 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {DAMMv1Lib, DAMMv1Pool, ERC20DAMMv1Pool} from "./damm_v1_pool.sol"; -import {ERC20SPLFactory} from "../erc20spl/erc20spl_factory.sol"; -import {ERC20Users} from "../erc20spl/erc20spl.sol"; -import {ICrossProgramInvocation} from "../interface.sol"; -import {SystemProgramLib} from "../system_program/system_program.sol"; -import {Convert} from "../convert.sol"; - -contract MeteoraDAMMv1Factory { - ERC20SPLFactory public immutable token_factory; - mapping(address => mapping(address => address)) public getPool; // token0 => token1 => pool - address[] public allPools; - - event PoolAdded( - address indexed token0, - address indexed token1, - address pair, - uint - ); - - bytes32 public prog_dynamic_vault; - bytes32 public prog_dynamic_amm; - address public cpi_program; - DAMMv1Lib.VaultOverrideNetwork public vault_override_network; - - constructor( - ERC20SPLFactory _token_factory, - bytes32 _prog_dynamic_vault, - bytes32 _prog_dynamic_amm, - address _cpi_program, - DAMMv1Lib.VaultOverrideNetwork _vault_override_network - ) { - token_factory = _token_factory; - prog_dynamic_vault = _prog_dynamic_vault; - prog_dynamic_amm = _prog_dynamic_amm; - cpi_program = _cpi_program; - vault_override_network = _vault_override_network; - } - - function allPoolsLength() external view returns (uint) { - return allPools.length; - } - - function deriveConfigKey(uint64 index) public view returns (bytes32) { - return DAMMv1Lib.derive_config_key(index, prog_dynamic_amm); - } - - function derivePermissionlessConstantProductPoolWithConfigKey( - bytes32 token_a_mint, - bytes32 token_b_mint, - bytes32 config - ) - public - view - returns (bytes32) - { - return DAMMv1Lib.derive_permissionless_constant_product_pool_with_config_key( - token_a_mint, - token_b_mint, - config, - prog_dynamic_amm - ); - } - - function orderTokens(address tokenA, address tokenB) public pure returns (address token0, address token1) { - (token0, token1) = tokenA < tokenB - ? (tokenA, tokenB) - : (tokenB, tokenA); - - return (token0, token1); - } - - function previewInitializeVault(bytes32 token_mint, address user) - external - view - returns ( - bool should_initialize, - DAMMv1Lib.InitializeVaultAccounts memory accounts_ - ) { - bytes32 payer = ERC20Users(token_factory.users()).get_user(user); - accounts_ = DAMMv1Lib.derive_initialize_vault_accounts( - token_mint, - payer, - prog_dynamic_vault, - vault_override_network - ); - should_initialize = _account_missing(accounts_.vault); - } - - function initializeVaultIfMissing(bytes32 token_mint) external returns (bytes32 vault, bool initialized_) { - bytes32 payer = token_factory.users().get_user(msg.sender); - DAMMv1Lib.InitializeVaultAccounts memory accounts_ = DAMMv1Lib.derive_initialize_vault_accounts( - token_mint, - payer, - prog_dynamic_vault, - vault_override_network - ); - - vault = accounts_.vault; - if (!_account_missing(vault)) { - return (vault, false); - } - - SystemProgramLib.Instruction memory ix = DAMMv1Lib.build_initialize_vault_instruction( - accounts_, - prog_dynamic_vault - ); - _invoke_signed(ix); - return (vault, true); - } - - function preparePermissionlessConstantProductPoolWithConfig2( - bytes32 token_a_mint, - bytes32 token_b_mint, - bytes32 config - ) - external - view - returns (DAMMv1Lib.InitializePermissionlessPoolWithConfigAccounts memory accounts_) - { - DAMMv1Lib.InitializePermissionlessPoolWithConfigConfig memory pool_config = - DAMMv1Lib.InitializePermissionlessPoolWithConfigConfig({ - token_a_mint: token_a_mint, - token_b_mint: token_b_mint, - payer: ERC20Users(token_factory.users()).get_user(msg.sender), - config: config, - dynamic_vault_program: prog_dynamic_vault, - dynamic_amm_program: prog_dynamic_amm, - override_network: vault_override_network - }); - - accounts_ = DAMMv1Lib.derive_initialize_permissionless_constant_product_pool_with_config_accounts(pool_config); - } - - function createPermissionlessConstantProductPoolWithConfig2( - uint64 token_a_amount, - uint64 token_b_amount, - DAMMv1Lib.InitializePermissionlessPoolWithConfigAccounts memory accounts - ) external returns (bytes32 pool_pubkey) { - require(!_account_missing(accounts.a_vault), "A_VAULT_MISSING"); - require(!_account_missing(accounts.b_vault), "B_VAULT_MISSING"); - require(_account_missing(accounts.pool), "POOL_EXISTS"); - - SystemProgramLib.Instruction memory ix = - DAMMv1Lib.build_initialize_permissionless_constant_product_pool_with_config2_instruction( - accounts, - token_a_amount, - token_b_amount, - prog_dynamic_amm - ); - _invoke_signed(ix); - pool_pubkey = accounts.pool; - } - - function addPool(bytes32 pubkey) external returns (address pool) { - return _register_pool(pubkey); - } - - function _account_missing(bytes32 pubkey) internal view returns (bool) { - (uint64 lamports,,,,,) = ICrossProgramInvocation(cpi_program).account_info(pubkey); - return lamports == 0; - } - - function _sameInitializePermissionlessPoolWithConfigAccounts( - DAMMv1Lib.InitializePermissionlessPoolWithConfigAccounts memory a, - DAMMv1Lib.InitializePermissionlessPoolWithConfigAccounts memory b - ) - internal - pure - returns (bool) - { - return a.pool == b.pool - && a.config == b.config - && a.lp_mint == b.lp_mint - && a.token_a_mint == b.token_a_mint - && a.token_b_mint == b.token_b_mint - && a.a_vault == b.a_vault - && a.b_vault == b.b_vault - && a.a_token_vault == b.a_token_vault - && a.b_token_vault == b.b_token_vault - && a.a_vault_lp_mint == b.a_vault_lp_mint - && a.b_vault_lp_mint == b.b_vault_lp_mint - && a.a_vault_lp == b.a_vault_lp - && a.b_vault_lp == b.b_vault_lp - && a.payer_token_a == b.payer_token_a - && a.payer_token_b == b.payer_token_b - && a.payer_pool_lp == b.payer_pool_lp - && a.protocol_token_a_fee == b.protocol_token_a_fee - && a.protocol_token_b_fee == b.protocol_token_b_fee - && a.payer == b.payer - && a.rent == b.rent - && a.vault_program == b.vault_program - && a.token_program == b.token_program - && a.associated_token_program == b.associated_token_program - && a.system_program == b.system_program - && a.metadata_program == b.metadata_program - && a.mint_metadata == b.mint_metadata; - } - - function _register_pool(bytes32 pubkey) internal returns (address pool) { - DAMMv1Lib.PoolState memory pool_state = DAMMv1Lib.load_pool(pubkey, cpi_program); - address token_a_address = token_factory.token_by_mint(pool_state.token_a_mint); - address token_b_address = token_factory.token_by_mint(pool_state.token_b_mint); - - require(token_a_address != address(0), "TokenA not registered in factory"); - require(token_b_address != address(0), "TokenB not registered in factory"); - - (address token0, address token1) = orderTokens(token_a_address, token_b_address); - require(getPool[token0][token1] == address(0), "PAIR_EXISTS"); - - bytes memory bytecode = type(DAMMv1Pool).creationCode; - bytes32 salt = keccak256(abi.encodePacked(token0, token1)); - assembly { - pool := create2(0, add(bytecode, 32), mload(bytecode), salt) - } - - DAMMv1Pool(pool).initialize( - address(token_factory), - pubkey, - prog_dynamic_vault, - prog_dynamic_amm, - cpi_program - ); - - ERC20DAMMv1Pool erc20_pool = new ERC20DAMMv1Pool(DAMMv1Pool(pool), token_factory); - getPool[token0][token1] = address(erc20_pool); - getPool[token1][token0] = address(erc20_pool); - allPools.push(address(erc20_pool)); - emit PoolAdded(token0, token1, address(erc20_pool), allPools.length); - - return address(erc20_pool); - } - - function _invoke_signed(SystemProgramLib.Instruction memory ix) internal { - bytes32[] memory seeds = new bytes32[](0); - - (bool success, bytes memory result) = address(cpi_program).delegatecall( - abi.encodeWithSelector( - ICrossProgramInvocation.invoke_signed.selector, - ix.program_id, - ix.accounts, - ix.data, - seeds - ) - ); - - require(success, string(Convert.revert_msg(result))); - } -} diff --git a/contracts/meteora/damm_v1_pool.sol b/contracts/meteora/damm_v1_pool.sol deleted file mode 100644 index 3a299dc..0000000 --- a/contracts/meteora/damm_v1_pool.sol +++ /dev/null @@ -1,1744 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "../interface.sol"; -import {SplTokenLib} from "../spl_token/spl_token.sol"; -import {Convert} from "../convert.sol"; -import "../rome_evm_account.sol"; -import {ERC20SPLFactory} from "../erc20spl/erc20spl_factory.sol"; -import {ERC20Users, SPL_ERC20} from "../erc20spl/erc20spl.sol"; -import {SystemProgramLib} from "../system_program/system_program.sol"; -import {AssociatedSplToken} from "../spl_token/associated_spl_token.sol"; -import {MplTokenMetadataLib} from "../mpl_token_metadata/lib.sol"; -import {ICrossProgramInvocation} from "../interface.sol"; -import {PdaDeriver} from "../cpi/PdaDeriver.sol"; -import {PdasBatch} from "../cpi/PdasBatch.sol"; -import {AccountReader} from "../cpi/AccountReader.sol"; - -library DAMMv1Lib { - bytes32 public constant PROG_DYNAMIC_AMM = - 0xccf802d4cccc84d7fb21b5f73b49d81a16c5b4c88ee32394e1c91d3588cc4080; // Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB - bytes32 public constant PROG_DYNAMIC_VAULT = - 0x0fbfe8846d685cbdc62cca7e04c7e8f68dcc313ab31277e2e0112a2ec0e052e5; // 24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi - bytes32 internal constant DYNAMIC_VAULT_BASE_KEY = - 0xf569dfde202333598dc7d74b1d94b8624779c1f82f1e25a65b6e4ef8a3be9b9b; // HWzXGcGHy4tcpYfaRDCyLNzXqBTv3E6BttpCH2vJxArv - - bytes8 public constant INITIALIZE_CONSTANT_PRODUCT_POOL_WITH_CONFIG2_PREFIX = bytes8(sha256(bytes("global:initialize_permissionless_constant_product_pool_with_config2"))); - bytes8 public constant SWAP_PREFIX = bytes8(sha256(bytes("global:swap"))); - bytes8 public constant ADD_BALANCE_LIQUIDITY_PREFIX = - bytes8(sha256(bytes("global:add_balance_liquidity"))); - bytes8 public constant REMOVE_BALANCE_LIQUIDITY_PREFIX = - bytes8(sha256(bytes("global:remove_balance_liquidity"))); - - uint256 internal constant POOL_PREFIX_MIN_LEN = 379; - // VAULT_MIN_LEN is the exact byte count parse_vault advances through. - // Doubles as the slice length load_vault requests from account_data_at. - // Layout (see parse_vault below): - // 8 (discriminator) + 3 (3× u8) + 8 (u64 total_amount) + 128 (4× bytes32) - // + 960 (strategies[30]) + 96 (base+admin+operator) + 24 (3× u64 locked_profit_tracker) - // = 1227 - // If this number ever drifts below the actual parser consumption, - // load_vault's slice will be too short and parse_vault reverts "oob u64". - uint256 internal constant VAULT_MIN_LEN = 1227; - uint64 internal constant DEFAULT_TRADE_FEE_BPS = 25; - bytes internal constant DYNAMIC_VAULT_VAULT_PREFIX = "vault"; - bytes internal constant DYNAMIC_VAULT_TOKEN_VAULT_PREFIX = "token_vault"; - bytes internal constant DYNAMIC_VAULT_LP_MINT_PREFIX = "lp_mint"; - bytes internal constant DYNAMIC_AMM_LP_MINT_PREFIX = "lp_mint"; - bytes internal constant DYNAMIC_AMM_PROTOCOL_FEE_PREFIX = "fee"; - bytes internal constant DYNAMIC_AMM_CONFIG_PREFIX = "config"; - - error InvalidVaultDataLength(uint256 actual, uint256 expected); - error InvalidPoolDataLength(uint256 actual, uint256 expected); - error InvalidPoolType(uint8 value); - - struct PoolFees { - uint64 trade_fee_numerator; - uint64 trade_fee_denominator; - uint64 protocol_trade_fee_numerator; - uint64 protocol_trade_fee_denominator; - } - - enum PoolType { - Permissioned, - Permissionless - } - - enum VaultOverrideNetwork { - Mainnet, - Devnet - } - - enum CurveType { - ConstantProduct, - Stable - } - - // DAMMv1 pool account state - struct PoolState { - bytes32 lp_mint; - bytes32 token_a_mint; - bytes32 token_b_mint; - bytes32 a_vault; - bytes32 b_vault; - bytes32 a_vault_lp; - bytes32 b_vault_lp; - uint8 a_vault_lp_bump; - bool enabled; - bytes32 protocol_token_a_fee; - bytes32 protocol_token_b_fee; - uint64 fee_last_updated_at; - PoolFees fees; - PoolType pool_type; - } - - struct VaultBumps { - uint8 vault_bump; - uint8 token_vault_bump; - } - - struct LockedProfitTracker { - uint64 last_updated_locked_profit; - uint64 last_report; - uint64 locked_profit_degradation; - } - - struct VaultState { - uint8 enabled; - VaultBumps bumps; - uint64 total_amount; - bytes32 token_vault; - bytes32 fee_vault; - bytes32 token_mint; - bytes32 lp_mint; - LockedProfitTracker locked_profit_tracker; - } - - // Helper structure used to prepare list of account for Swap instruction - struct SwapAccountsInput { - bytes32 pool; - bytes32 user_source_token; - bytes32 user_destination_token; - bytes32 a_vault_lp; - bytes32 b_vault_lp; - bytes32 a_vault; - bytes32 b_vault; - bytes32 a_vault_lp_mint; - bytes32 b_vault_lp_mint; - bytes32 a_token_vault; - bytes32 b_token_vault; - bytes32 user; - bytes32 vault_program; - bytes32 token_program; - bytes32 protocol_token_fee; - } - - // Shared account layout for balanced add/remove liquidity. - struct BalanceLiquidityAccountsInput { - bytes32 pool; - bytes32 lp_mint; - bytes32 user_pool_lp; - bytes32 a_vault_lp; - bytes32 b_vault_lp; - bytes32 a_vault; - bytes32 b_vault; - bytes32 a_vault_lp_mint; - bytes32 b_vault_lp_mint; - bytes32 a_token_vault; - bytes32 b_token_vault; - bytes32 user_a_token; - bytes32 user_b_token; - bytes32 user; - bytes32 vault_program; - bytes32 token_program; - } - - // User-supplied token accounts for the optimized addLiquidity flow. - struct AddLiquidityUserAccountsInput { - bytes32 user_pool_lp; - bytes32 user_a_token; - bytes32 user_b_token; - } - - struct InitializeVaultAccounts { - bytes32 vault; - bytes32 payer; - bytes32 token_vault; - bytes32 token_mint; - bytes32 lp_mint; - bytes32 rent; - bytes32 token_program; - bytes32 system_program; - } - - struct InitializePermissionlessPoolAccounts { - bytes32 pool; - bytes32 lp_mint; - bytes32 token_a_mint; - bytes32 token_b_mint; - bytes32 a_vault; - bytes32 b_vault; - bytes32 a_token_vault; - bytes32 b_token_vault; - bytes32 a_vault_lp_mint; - bytes32 b_vault_lp_mint; - bytes32 a_vault_lp; - bytes32 b_vault_lp; - bytes32 payer_token_a; - bytes32 payer_token_b; - bytes32 payer_pool_lp; - bytes32 protocol_token_a_fee; - bytes32 protocol_token_b_fee; - bytes32 payer; - bytes32 fee_owner; - bytes32 rent; - bytes32 vault_program; - bytes32 token_program; - bytes32 associated_token_program; - bytes32 system_program; - bytes32 metadata_program; - bytes32 mint_metadata; - } - - struct InitializePermissionlessPoolConfig { - CurveType curve_type; - bytes32 token_a_mint; - bytes32 token_b_mint; - uint64 trade_fee_bps; - uint64 token_a_amount; - uint64 token_b_amount; - bytes32 payer; - bytes32 fee_owner; - bytes32 dynamic_vault_program; - bytes32 dynamic_amm_program; - VaultOverrideNetwork override_network; - } - - struct PermissionlessPoolDerivedKeys { - bytes32 pool; - bytes32 lp_mint; - bytes32 a_vault; - bytes32 b_vault; - bytes32 a_token_vault; - bytes32 b_token_vault; - bytes32 a_vault_lp_mint; - bytes32 b_vault_lp_mint; - bytes32 a_vault_lp; - bytes32 b_vault_lp; - bytes32 protocol_token_a_fee; - bytes32 protocol_token_b_fee; - bytes32 mint_metadata; - } - - struct InitializePermissionlessPoolWithConfigAccounts { - bytes32 pool; - bytes32 config; - bytes32 lp_mint; - bytes32 token_a_mint; - bytes32 token_b_mint; - bytes32 a_vault; - bytes32 b_vault; - bytes32 a_token_vault; - bytes32 b_token_vault; - bytes32 a_vault_lp_mint; - bytes32 b_vault_lp_mint; - bytes32 a_vault_lp; - bytes32 b_vault_lp; - bytes32 payer_token_a; - bytes32 payer_token_b; - bytes32 payer_pool_lp; - bytes32 protocol_token_a_fee; - bytes32 protocol_token_b_fee; - bytes32 payer; - bytes32 rent; - bytes32 vault_program; - bytes32 token_program; - bytes32 associated_token_program; - bytes32 system_program; - bytes32 metadata_program; - bytes32 mint_metadata; - } - - struct InitializePermissionlessPoolWithConfigConfig { - bytes32 token_a_mint; - bytes32 token_b_mint; - bytes32 payer; - bytes32 config; - bytes32 dynamic_vault_program; - bytes32 dynamic_amm_program; - VaultOverrideNetwork override_network; - } - - struct PermissionlessPoolWithConfigDerivedKeys { - bytes32 pool; - bytes32 lp_mint; - bytes32 a_vault; - bytes32 b_vault; - bytes32 a_token_vault; - bytes32 b_token_vault; - bytes32 a_vault_lp_mint; - bytes32 b_vault_lp_mint; - bytes32 a_vault_lp; - bytes32 b_vault_lp; - bytes32 protocol_token_a_fee; - bytes32 protocol_token_b_fee; - bytes32 mint_metadata; - } - - // Parses DAMMv1 pool - // @param data raw Solana account data - // @return instance PoolState structure - function parse_pool(bytes memory data) pure internal returns (PoolState memory p) - { - if (data.length < POOL_PREFIX_MIN_LEN) { - revert InvalidPoolDataLength(data.length, POOL_PREFIX_MIN_LEN); - } - - uint256 o = 8; // anchor discriminator - - (p.lp_mint, o) = Convert.read_bytes32(data, o); - (p.token_a_mint, o) = Convert.read_bytes32(data, o); - (p.token_b_mint, o) = Convert.read_bytes32(data, o); - (p.a_vault, o) = Convert.read_bytes32(data, o); - (p.b_vault, o) = Convert.read_bytes32(data, o); - (p.a_vault_lp, o) = Convert.read_bytes32(data, o); - (p.b_vault_lp, o) = Convert.read_bytes32(data, o); - - (p.a_vault_lp_bump, o) = Convert.read_u8(data, o); - - uint8 enabled_u8; - (enabled_u8, o) = Convert.read_u8(data, o); - p.enabled = enabled_u8 != 0; - - (p.protocol_token_a_fee, o) = Convert.read_bytes32(data, o); - (p.protocol_token_b_fee, o) = Convert.read_bytes32(data, o); - (p.fee_last_updated_at, o) = Convert.read_u64le(data, o); - - o += 24; // _padding0 - - (p.fees.trade_fee_numerator, o) = Convert.read_u64le(data, o); - (p.fees.trade_fee_denominator, o) = Convert.read_u64le(data, o); - (p.fees.protocol_trade_fee_numerator, o) = Convert.read_u64le(data, o); - (p.fees.protocol_trade_fee_denominator, o) = Convert.read_u64le(data, o); - - uint8 pool_type_u8; - (pool_type_u8, o) = Convert.read_u8(data, o); - if (pool_type_u8 > uint8(PoolType.Permissionless)) { - revert InvalidPoolType(pool_type_u8); - } - p.pool_type = PoolType(pool_type_u8); - - return p; - } - - function parse_vault(bytes memory data) - internal - pure - returns (VaultState memory v) - { - if (data.length < VAULT_MIN_LEN) { - revert InvalidVaultDataLength(data.length, VAULT_MIN_LEN); - } - - uint256 o = 8; // anchor discriminator - - (v.enabled, o) = Convert.read_u8(data, o); - (v.bumps.vault_bump, o) = Convert.read_u8(data, o); - (v.bumps.token_vault_bump, o) = Convert.read_u8(data, o); - - (v.total_amount, o) = Convert.read_u64le(data, o); - (v.token_vault, o) = Convert.read_bytes32(data, o); - (v.fee_vault, o) = Convert.read_bytes32(data, o); - (v.token_mint, o) = Convert.read_bytes32(data, o); - (v.lp_mint, o) = Convert.read_bytes32(data, o); - - o += 32 * 30; // strategies[30] - o += 32; // base - o += 32; // admin - o += 32; // operator - - (v.locked_profit_tracker.last_updated_locked_profit, o) = Convert.read_u64le(data, o); - (v.locked_profit_tracker.last_report, o) = Convert.read_u64le(data, o); - (v.locked_profit_tracker.locked_profit_degradation, o) = Convert.read_u64le(data, o); - - return v; - } - - // Reads DAMMv1 pool account from Solana and parses it - // @param pool_pubkey address of DAMMv1 Pool account in Solana - // @return instance PoolState structure - function load_pool(bytes32 pool_pubkey, address /* cpi_program (unused — typed slice via AccountReader) */) - internal - view - returns (PoolState memory) { - // Slice-only read via account_data_at (selector 0x593762e8) — skips - // the lamports/owner/flags fields of the prior account_info 6-tuple - // since parse_pool only consumes data[0..POOL_PREFIX_MIN_LEN]. - // Saves ~10-15K CU per load_pool call (Agent 3 audit, 2026-05-16). - bytes memory data = AccountReader.readBytesAt(pool_pubkey, 0, uint16(POOL_PREFIX_MIN_LEN)); - return parse_pool(data); - } - - function load_vault(bytes32 vault_pubkey, address /* cpi_program (unused — typed slice via AccountReader) */) - internal - view - returns (VaultState memory) - { - // Slice-only read via account_data_at. parse_vault consumes the full - // VAULT_MIN_LEN-byte prefix (including the 960-byte strategies skip); - // saving ~15-25K CU per load_vault call vs the prior account_info - // 6-tuple decode (Agent 3 audit, 2026-05-16). - bytes memory data = AccountReader.readBytesAt(vault_pubkey, 0, uint16(VAULT_MIN_LEN)); - return parse_vault(data); - } - - function derive_vault_key(bytes32 mint, bytes32 dynamic_vault_program) - internal - pure - returns (bytes32) - { - ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](3); - seeds[0] = ISystemProgram.Seed(DYNAMIC_VAULT_VAULT_PREFIX); - seeds[1] = ISystemProgram.Seed(abi.encodePacked(mint)); - seeds[2] = ISystemProgram.Seed(abi.encodePacked(DYNAMIC_VAULT_BASE_KEY)); - - (bytes32 vaultKey,) = SystemProgram.find_program_address(dynamic_vault_program, seeds); - return vaultKey; - } - - function derive_config_key(uint64 index, bytes32 dynamic_amm_program) - internal - pure - returns (bytes32) - { - ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](2); - seeds[0] = ISystemProgram.Seed(DYNAMIC_AMM_CONFIG_PREFIX); - seeds[1] = ISystemProgram.Seed(abi.encodePacked(Convert.u64le(index))); - - (bytes32 configKey,) = SystemProgram.find_program_address(dynamic_amm_program, seeds); - return configKey; - } - - function derive_permissionless_constant_product_pool_with_config_key( - bytes32 token_a_mint, - bytes32 token_b_mint, - bytes32 config, - bytes32 dynamic_amm_program - ) - internal - pure - returns (bytes32) - { - ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](3); - seeds[0] = ISystemProgram.Seed(abi.encodePacked(get_first_key(token_a_mint, token_b_mint))); - seeds[1] = ISystemProgram.Seed(abi.encodePacked(get_second_key(token_a_mint, token_b_mint))); - seeds[2] = ISystemProgram.Seed(abi.encodePacked(config)); - - (bytes32 poolKey,) = SystemProgram.find_program_address(dynamic_amm_program, seeds); - return poolKey; - } - - function derive_pool_lp_mint_key(bytes32 pool, bytes32 dynamic_amm_program) - internal - pure - returns (bytes32) - { - ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](2); - seeds[0] = ISystemProgram.Seed(DYNAMIC_AMM_LP_MINT_PREFIX); - seeds[1] = ISystemProgram.Seed(abi.encodePacked(pool)); - - (bytes32 lpMintKey,) = SystemProgram.find_program_address(dynamic_amm_program, seeds); - return lpMintKey; - } - - function derive_vault_lp_key(bytes32 vault, bytes32 pool, bytes32 dynamic_amm_program) - internal - pure - returns (bytes32) - { - ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](2); - seeds[0] = ISystemProgram.Seed(abi.encodePacked(vault)); - seeds[1] = ISystemProgram.Seed(abi.encodePacked(pool)); - - (bytes32 vaultLpKey,) = SystemProgram.find_program_address(dynamic_amm_program, seeds); - return vaultLpKey; - } - - function derive_protocol_fee_key(bytes32 token_mint, bytes32 pool, bytes32 dynamic_amm_program) - internal - pure - returns (bytes32) - { - ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](3); - seeds[0] = ISystemProgram.Seed(DYNAMIC_AMM_PROTOCOL_FEE_PREFIX); - seeds[1] = ISystemProgram.Seed(abi.encodePacked(token_mint)); - seeds[2] = ISystemProgram.Seed(abi.encodePacked(pool)); - - (bytes32 protocolFeeKey,) = SystemProgram.find_program_address(dynamic_amm_program, seeds); - return protocolFeeKey; - } - - function derive_token_vault_key(bytes32 vault, bytes32 dynamic_vault_program) - internal - pure - returns (bytes32) - { - ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](2); - seeds[0] = ISystemProgram.Seed(DYNAMIC_VAULT_TOKEN_VAULT_PREFIX); - seeds[1] = ISystemProgram.Seed(abi.encodePacked(vault)); - - (bytes32 tokenVaultKey,) = SystemProgram.find_program_address(dynamic_vault_program, seeds); - return tokenVaultKey; - } - - function derive_lp_mint_key( - bytes32 vault, - bytes32 dynamic_vault_program, - VaultOverrideNetwork override_network - ) - internal - pure - returns (bytes32) - { - bytes32 non_derived_based_lp_mint = lookup_non_pda_based_lp_mint(vault, override_network); - if (non_derived_based_lp_mint != bytes32(0)) { - return non_derived_based_lp_mint; - } - - ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](2); - seeds[0] = ISystemProgram.Seed(DYNAMIC_VAULT_LP_MINT_PREFIX); - seeds[1] = ISystemProgram.Seed(abi.encodePacked(vault)); - - (bytes32 lpMintKey,) = SystemProgram.find_program_address(dynamic_vault_program, seeds); - return lpMintKey; - } - - function derive_initialize_vault_accounts( - bytes32 token_mint, - bytes32 payer, - bytes32 dynamic_vault_program, - VaultOverrideNetwork override_network - ) - internal - pure - returns (InitializeVaultAccounts memory accounts_) - { - bytes32 vault = derive_vault_key(token_mint, dynamic_vault_program); - - accounts_ = InitializeVaultAccounts({ - vault: vault, - payer: payer, - token_vault: derive_token_vault_key(vault, dynamic_vault_program), - token_mint: token_mint, - lp_mint: derive_lp_mint_key(vault, dynamic_vault_program, override_network), - rent: SystemProgramLib.RENT_ID, - token_program: SplTokenLib.SPL_TOKEN_PROGRAM, - system_program: SystemProgramLib.PROGRAM_ID - }); - } - - function derive_initialize_permissionless_constant_product_pool_with_config_accounts( - InitializePermissionlessPoolWithConfigConfig memory config - ) - internal - view - returns (InitializePermissionlessPoolWithConfigAccounts memory accounts_) - { - PermissionlessPoolWithConfigDerivedKeys memory derived = _derive_permissionless_pool_with_config_keys(config); - - accounts_.pool = derived.pool; - accounts_.config = config.config; - accounts_.lp_mint = derived.lp_mint; - accounts_.token_a_mint = config.token_a_mint; - accounts_.token_b_mint = config.token_b_mint; - accounts_.a_vault = derived.a_vault; - accounts_.b_vault = derived.b_vault; - accounts_.a_token_vault = derived.a_token_vault; - accounts_.b_token_vault = derived.b_token_vault; - accounts_.a_vault_lp_mint = derived.a_vault_lp_mint; - accounts_.b_vault_lp_mint = derived.b_vault_lp_mint; - accounts_.a_vault_lp = derived.a_vault_lp; - accounts_.b_vault_lp = derived.b_vault_lp; - accounts_.payer_token_a = AssociatedSplToken.get_associated_token_address_with_program_id( - config.payer, - config.token_a_mint, - SplTokenLib.SPL_TOKEN_PROGRAM, - AssociatedSplToken.ASSOCIATED_TOKEN_PROGRAM_ID - ); - accounts_.payer_token_b = AssociatedSplToken.get_associated_token_address_with_program_id( - config.payer, - config.token_b_mint, - SplTokenLib.SPL_TOKEN_PROGRAM, - AssociatedSplToken.ASSOCIATED_TOKEN_PROGRAM_ID - ); - accounts_.payer_pool_lp = AssociatedSplToken.get_associated_token_address_with_program_id( - config.payer, - derived.lp_mint, - SplTokenLib.SPL_TOKEN_PROGRAM, - AssociatedSplToken.ASSOCIATED_TOKEN_PROGRAM_ID - ); - accounts_.protocol_token_a_fee = derived.protocol_token_a_fee; - accounts_.protocol_token_b_fee = derived.protocol_token_b_fee; - accounts_.payer = config.payer; - accounts_.rent = SystemProgramLib.RENT_ID; - accounts_.vault_program = config.dynamic_vault_program; - accounts_.token_program = SplTokenLib.SPL_TOKEN_PROGRAM; - accounts_.associated_token_program = AssociatedSplToken.ASSOCIATED_TOKEN_PROGRAM_ID; - accounts_.system_program = SystemProgramLib.PROGRAM_ID; - accounts_.metadata_program = MplTokenMetadataLib.MPL_TOKEN_METADATA_PROGRAM_ID; - accounts_.mint_metadata = derived.mint_metadata; - } - - function _derive_permissionless_pool_with_config_keys( - InitializePermissionlessPoolWithConfigConfig memory config - ) - private - view - returns (PermissionlessPoolWithConfigDerivedKeys memory derived) - { - // Step 1: pool (1 derivation on dynamic_amm_program; can't batch — - // depends on token_a + token_b + config, no peer derivations). - derived.pool = derive_permissionless_constant_product_pool_with_config_key( - config.token_a_mint, - config.token_b_mint, - config.config, - config.dynamic_amm_program - ); - - // Step 2 — BATCH A — a_vault + b_vault on dynamic_vault_program. - // PdasBatch.pair replaces 2 separate find_program_address syscalls. - { - ISystemProgram.Seed[] memory aVaultSeeds = PdaDeriver.makeSeeds( - PdaDeriver.seedBytesRaw(DYNAMIC_VAULT_VAULT_PREFIX), - PdaDeriver.seedBytes(config.token_a_mint), - PdaDeriver.seedBytes(DYNAMIC_VAULT_BASE_KEY) - ); - ISystemProgram.Seed[] memory bVaultSeeds = PdaDeriver.makeSeeds( - PdaDeriver.seedBytesRaw(DYNAMIC_VAULT_VAULT_PREFIX), - PdaDeriver.seedBytes(config.token_b_mint), - PdaDeriver.seedBytes(DYNAMIC_VAULT_BASE_KEY) - ); - ( - ICrossProgramInvocation.PdaWithBump memory aVault, - ICrossProgramInvocation.PdaWithBump memory bVault - ) = PdasBatch.pair(aVaultSeeds, bVaultSeeds, config.dynamic_vault_program); - derived.a_vault = aVault.pda; - derived.b_vault = bVault.pda; - } - - // Step 3 — BATCH B — a_token_vault + b_token_vault on - // dynamic_vault_program (depends on a_vault + b_vault from Batch A). - { - ISystemProgram.Seed[] memory aTokenVaultSeeds = PdaDeriver.makeSeeds( - PdaDeriver.seedBytesRaw(DYNAMIC_VAULT_TOKEN_VAULT_PREFIX), - PdaDeriver.seedBytes(derived.a_vault) - ); - ISystemProgram.Seed[] memory bTokenVaultSeeds = PdaDeriver.makeSeeds( - PdaDeriver.seedBytesRaw(DYNAMIC_VAULT_TOKEN_VAULT_PREFIX), - PdaDeriver.seedBytes(derived.b_vault) - ); - ( - ICrossProgramInvocation.PdaWithBump memory aTokenVault, - ICrossProgramInvocation.PdaWithBump memory bTokenVault - ) = PdasBatch.pair(aTokenVaultSeeds, bTokenVaultSeeds, config.dynamic_vault_program); - derived.a_token_vault = aTokenVault.pda; - derived.b_token_vault = bTokenVault.pda; - } - - // Step 4: a/b_vault_lp_mint — kept on the conditional helper because - // `derive_lp_mint_key` short-circuits to a hardcoded value for the - // Devnet override (`lookup_non_pda_based_lp_mint`). Batching would - // unconditionally derive — semantically incorrect for Devnet pools. - derived.a_vault_lp_mint = derive_lp_mint_key( - derived.a_vault, - config.dynamic_vault_program, - config.override_network - ); - derived.b_vault_lp_mint = derive_lp_mint_key( - derived.b_vault, - config.dynamic_vault_program, - config.override_network - ); - - // Step 5 — BATCH C — 5 PDAs on dynamic_amm_program (all depend on - // derived.pool from Step 1 and a_vault/b_vault from Batch A). - // PdasBatch.derive replaces 5 separate find_program_address syscalls. - { - ISystemProgram.Seed[][] memory groups = new ISystemProgram.Seed[][](5); - - // [0] lp_mint = [DYNAMIC_AMM_LP_MINT_PREFIX, pool] - groups[0] = PdaDeriver.makeSeeds( - PdaDeriver.seedBytesRaw(DYNAMIC_AMM_LP_MINT_PREFIX), - PdaDeriver.seedBytes(derived.pool) - ); - // [1] a_vault_lp = [a_vault, pool] - groups[1] = PdaDeriver.makeSeeds( - PdaDeriver.seedBytes(derived.a_vault), - PdaDeriver.seedBytes(derived.pool) - ); - // [2] b_vault_lp = [b_vault, pool] - groups[2] = PdaDeriver.makeSeeds( - PdaDeriver.seedBytes(derived.b_vault), - PdaDeriver.seedBytes(derived.pool) - ); - // [3] protocol_token_a_fee = [PROTOCOL_FEE_PREFIX, token_a_mint, pool] - groups[3] = PdaDeriver.makeSeeds( - PdaDeriver.seedBytesRaw(DYNAMIC_AMM_PROTOCOL_FEE_PREFIX), - PdaDeriver.seedBytes(config.token_a_mint), - PdaDeriver.seedBytes(derived.pool) - ); - // [4] protocol_token_b_fee = [PROTOCOL_FEE_PREFIX, token_b_mint, pool] - groups[4] = PdaDeriver.makeSeeds( - PdaDeriver.seedBytesRaw(DYNAMIC_AMM_PROTOCOL_FEE_PREFIX), - PdaDeriver.seedBytes(config.token_b_mint), - PdaDeriver.seedBytes(derived.pool) - ); - - ICrossProgramInvocation.PdaWithBump[] memory ammPhase = - PdasBatch.derive(groups, config.dynamic_amm_program); - derived.lp_mint = ammPhase[0].pda; - derived.a_vault_lp = ammPhase[1].pda; - derived.b_vault_lp = ammPhase[2].pda; - derived.protocol_token_a_fee = ammPhase[3].pda; - derived.protocol_token_b_fee = ammPhase[4].pda; - } - - // Step 6: mint_metadata (Metaplex Token Metadata program — different - // program, can't batch with the dynamic_amm_program group). - (derived.mint_metadata,) = MplTokenMetadataLib.find_metadata_pda( - derived.lp_mint, - MplTokenMetadataLib.MPL_TOKEN_METADATA_PROGRAM_ID - ); - } - - function build_initialize_vault_instruction( - InitializeVaultAccounts memory a, - bytes32 dynamic_vault_program - ) - internal - pure - returns (SystemProgramLib.Instruction memory ix) - { - ix.program_id = dynamic_vault_program; - ix.accounts = build_initialize_vault_account_metas(a); - ix.data = build_initialize_vault_ix_data(); - } - - - function build_initialize_permissionless_constant_product_pool_with_config2_instruction( - InitializePermissionlessPoolWithConfigAccounts memory a, - uint64 token_a_amount, - uint64 token_b_amount, - bytes32 dynamic_amm_program - ) - internal - pure - returns (SystemProgramLib.Instruction memory ix) - { - ix.program_id = dynamic_amm_program; - ix.accounts = build_initialize_permissionless_constant_product_pool_with_config_account_metas(a); - ix.data = build_initialize_permissionless_constant_product_pool_with_config2_ix_data( - token_a_amount, - token_b_amount - ); - } - - function build_initialize_vault_account_metas(InitializeVaultAccounts memory a) - internal - pure - returns (ICrossProgramInvocation.AccountMeta[] memory metas) - { - metas = new ICrossProgramInvocation.AccountMeta[](8); - metas[0] = ICrossProgramInvocation.AccountMeta(a.vault, false, true); - metas[1] = ICrossProgramInvocation.AccountMeta(a.payer, true, true); - metas[2] = ICrossProgramInvocation.AccountMeta(a.token_vault, false, true); - metas[3] = ICrossProgramInvocation.AccountMeta(a.token_mint, false, false); - metas[4] = ICrossProgramInvocation.AccountMeta(a.lp_mint, false, true); - metas[5] = ICrossProgramInvocation.AccountMeta(a.rent, false, false); - metas[6] = ICrossProgramInvocation.AccountMeta(a.token_program, false, false); - metas[7] = ICrossProgramInvocation.AccountMeta(a.system_program, false, false); - } - - function build_initialize_vault_ix_data() - internal - pure - returns (bytes memory) - { - return abi.encodePacked(bytes8(sha256(bytes("global:initialize")))); - } - - function build_initialize_permissionless_constant_product_pool_with_config_account_metas( - InitializePermissionlessPoolWithConfigAccounts memory a - ) - internal - pure - returns (ICrossProgramInvocation.AccountMeta[] memory metas) - { - metas = new ICrossProgramInvocation.AccountMeta[](26); - metas[0] = ICrossProgramInvocation.AccountMeta(a.pool, false, true); - metas[1] = ICrossProgramInvocation.AccountMeta(a.config, false, false); - metas[2] = ICrossProgramInvocation.AccountMeta(a.lp_mint, false, true); - metas[3] = ICrossProgramInvocation.AccountMeta(a.token_a_mint, false, false); - metas[4] = ICrossProgramInvocation.AccountMeta(a.token_b_mint, false, false); - metas[5] = ICrossProgramInvocation.AccountMeta(a.a_vault, false, true); - metas[6] = ICrossProgramInvocation.AccountMeta(a.b_vault, false, true); - metas[7] = ICrossProgramInvocation.AccountMeta(a.a_token_vault, false, true); - metas[8] = ICrossProgramInvocation.AccountMeta(a.b_token_vault, false, true); - metas[9] = ICrossProgramInvocation.AccountMeta(a.a_vault_lp_mint, false, true); - metas[10] = ICrossProgramInvocation.AccountMeta(a.b_vault_lp_mint, false, true); - metas[11] = ICrossProgramInvocation.AccountMeta(a.a_vault_lp, false, true); - metas[12] = ICrossProgramInvocation.AccountMeta(a.b_vault_lp, false, true); - metas[13] = ICrossProgramInvocation.AccountMeta(a.payer_token_a, false, true); - metas[14] = ICrossProgramInvocation.AccountMeta(a.payer_token_b, false, true); - metas[15] = ICrossProgramInvocation.AccountMeta(a.payer_pool_lp, false, true); - metas[16] = ICrossProgramInvocation.AccountMeta(a.protocol_token_a_fee, false, true); - metas[17] = ICrossProgramInvocation.AccountMeta(a.protocol_token_b_fee, false, true); - metas[18] = ICrossProgramInvocation.AccountMeta(a.payer, true, true); - metas[19] = ICrossProgramInvocation.AccountMeta(a.rent, false, false); - metas[20] = ICrossProgramInvocation.AccountMeta(a.mint_metadata, false, true); - metas[21] = ICrossProgramInvocation.AccountMeta(a.metadata_program, false, false); - metas[22] = ICrossProgramInvocation.AccountMeta(a.vault_program, false, false); - metas[23] = ICrossProgramInvocation.AccountMeta(a.token_program, false, false); - metas[24] = ICrossProgramInvocation.AccountMeta(a.associated_token_program, false, false); - metas[25] = ICrossProgramInvocation.AccountMeta(a.system_program, false, false); - } - - function build_initialize_permissionless_constant_product_pool_with_config2_ix_data( - uint64 token_a_amount, - uint64 token_b_amount - ) - internal - pure - returns (bytes memory) - { - return abi.encodePacked( - INITIALIZE_CONSTANT_PRODUCT_POOL_WITH_CONFIG2_PREFIX, - Convert.u64le(token_a_amount), - Convert.u64le(token_b_amount), - uint8(0) // Option::None for activation_point - ); - } - - function get_curve_type_bytes(CurveType curve_type) - internal - pure - returns (bytes memory) - { - return abi.encodePacked(uint8(curve_type == CurveType.ConstantProduct ? 0 : 1)); - } - - function get_first_key(bytes32 token_a_mint, bytes32 token_b_mint) - internal - pure - returns (bytes32) - { - return uint256(token_a_mint) > uint256(token_b_mint) ? token_a_mint : token_b_mint; - } - - function get_second_key(bytes32 token_a_mint, bytes32 token_b_mint) - internal - pure - returns (bytes32) - { - return uint256(token_a_mint) > uint256(token_b_mint) ? token_b_mint : token_a_mint; - } - - function get_trade_fee_bps_bytes(uint64 trade_fee_bps) - internal - pure - returns (bytes memory) - { - if (trade_fee_bps == DEFAULT_TRADE_FEE_BPS) { - return bytes(""); - } - - return abi.encodePacked(Convert.u64le(trade_fee_bps)); - } - - function lookup_non_pda_based_lp_mint(bytes32 vault, VaultOverrideNetwork override_network) - internal - pure - returns (bytes32) - { - if (override_network == VaultOverrideNetwork.Devnet) { - return lookup_non_pda_based_lp_mint_devnet(vault); - } - - return lookup_non_pda_based_lp_mint_mainnet(vault); - } - - function lookup_non_pda_based_lp_mint_mainnet(bytes32 vault) - internal - pure - returns (bytes32) - { - if (vault == 0x983ea73c317a5fd691bd2b3f19a3e23b6ba1652d898c15fbb010da925944dc2d) return 0x3e7bbb27b0fc7d689e0be8d710540b5292d10abb317eeab8d02cf2a4cf7bf702; - if (vault == 0x948b3a136a9c978dd13171ccf112ba2a7dbd20ced5e2d97e628281542b738e37) return 0x7c1a5cd7617b51fa44b8788167de5686857877379ef8cc63d34047934543b4d6; - if (vault == 0xe2d1c42533a7afd796067a6c076af65f01f849d3b8cc1896c7c62b96c707d326) return 0x35f174c1e6289a0b756ae98ba6b4f26513ad526bc4d2a68f9916bd181d1edd03; - if (vault == 0xead3ffd164e7f73a2b61b5c3812f8dea76ff8339ad8c36d63a41db88cab416d5) return 0x9fffe6fd0c0ad672aae483067eb47237325c4b4e3c759dbad848b5f2ee36e170; - if (vault == 0x4bd44319514e16bdf0990bb8e9a7a670297549c2639a999f2fd6e7ddf6800001) return 0x7c7f14872012ba046e65e945c341aab8500e8df5d7b869809c9ae0a7f936201c; - if (vault == 0x1826bccf8efce0bfdd6069eaf85e36eb2b95952eba66d3e6ad3d5f28635998b2) return 0x8f9f2e1b7ed7e0e7ed834ba7d5644f7bd837b9e43c5596bc87b160cf127e2b46; - if (vault == 0x9bd48879dbe2817e9d689e444035373e3449d23917f4f203089d103c31c5bb00) return 0xd3cc1eda8d06495adc0c57375f04231dadd82c30af8280e43907271db63b61e2; - if (vault == 0x432b0a1f9d68038a747e26a6a83a5348e799b2e14b811c8cee0bdb00e9012097) return 0xc9963109f8909b3ca35001534d3fa75c8ae4942255b9c164aa220cfc7c4b4a2a; - if (vault == 0x0b5f00045110363bfba9ca6bed530cd5c4592702830b43089441782937189ec6) return 0x3826ef984eb5b55bc3e6ec6ac5f98dddcbe8cf6b69a12bfde0174cca262413fc; - if (vault == 0x740ca89675e5a15da7ccbc2572d26e715bea81fe457d1ac384e75ee2b4669f34) return 0x0f029976c29c2e9f2b89a1c0832d53e5961d9c194e8966794946bf5a0a36504a; - if (vault == 0xa76b638b491a5d90979e2749adb0db8d4bf126b4388fd718132ed35e3edcdb81) return 0x10bb92f3f4ee9ee5fa8baadb4b4adeed946f351746f8a0d7fc3d8fa1a2f8f59f; - if (vault == 0xd3742176b89c5e14c96b748998421f38101e09e5c59331b168b625cd2715bb26) return 0xd84e1290c198e788ba0e114f038214679608da28fcd20a1631efc393fbbf692a; - if (vault == 0x2128b91cdeb1909ca90d852dedecca5b733a3816ca84e64b28a81a270dfd0dff) return 0x2412b8f35b076ddbface5f59862d529f46f66a1dd628d37b050f946734bde847; - - return bytes32(0); - } - - function lookup_non_pda_based_lp_mint_devnet(bytes32 vault) - internal - pure - returns (bytes32) - { - if (vault == 0x1c3809c7c083cb8cf8cb830e948e264039b45249101bb16bdcaa95bbabd6c8a9) return 0xb597a7b52e468c31723d1b4ee2e226b5230aa9707e97831e6a2f182f37ec2d03; - if (vault == 0x0d0689cb4491a4bc4457f2190b52d6ee00f60296eb84ac9f9eff11a83566e1b9) return 0x24d05249cefb6683bae393083e79dc96b62a299884a5458884883686c7d7786b; - if (vault == 0xe01cd03768a84cbe2f2a967d35b3dc6b802e1883acbd9edb9b4cedd25b95093b) return 0xa3930692ed460fd10d6ba0a83d0c2ade3eaa1968d417e835412e46df2ded17f6; - if (vault == 0x12a0c9329b5bf995e1d4642054734a3e6367d76f0e415a06247d4ac1ab3ed043) return 0xbe9e98af1d7bc700f33c50f144337a535fb417ac05cc2788a0159c0f5f8eb856; - if (vault == 0x8c8c93b41c3a6d4730c0707ff74a333d6b81218fb560e75a82484a5f33b4c35c) return 0x9eac0f60f777ee0fe1f244cb25efd78621e0a9106fd7d2f40d5053b0fc91c6df; - if (vault == 0xd3742176b89c5e14c96b748998421f38101e09e5c59331b168b625cd2715bb26) return 0x02cca2aaece19457f1f3d1f73f1b86f47d7a17838b0b9ad4a00320a6829e30fe; - if (vault == 0x740ca89675e5a15da7ccbc2572d26e715bea81fe457d1ac384e75ee2b4669f34) return 0x7001a581accf0a08c77b528e23d46f2e6af6c00852357744defe068abc755cdd; - if (vault == 0x35ac52518afbe7485c4f4ad6a5c339898ebe06fb4d32ac79eec4c1efdaa6c36a) return 0x196e009889f4c769756a02d1e0df538eea88aa93b3768f61d5ad21a694c9bc9c; - if (vault == 0x7ab4cc7f65219c0c82ed9a6a3fcf81a85df638d0299acb4ff447fdb742887d5f) return 0xa8e5e0f91ec83af7e8b0315995b31ab2b2c9ea963ca2a721aae0748f71f9a53f; - if (vault == 0x9a4fc20963decd1ab7c5d34c3c01c1fd3e0b61dfed1ff7c45867c9c337a0108a) return 0x0d3b861351ac9bb496ea233569c774efef4d8e3ccad75350ad225f2f22422c1a; - if (vault == 0xbabcd7961c5e0a60657a8c94e31549bfcc275136f944a21c9f78e411e48c32d6) return 0xe206c2d378d04a4c87fea8d3eaae21a3b7e79710397da50790fefc167a39f4d2; - if (vault == 0xb1d3f5c9ed4d9351f4d4dcc82919352e694f998b538bf5d3f29873c4b1e5fdef) return 0xf3bdfc07b6a99dcec73ebcedf2726d41a6d0bc9987006d824d18117951179e8f; - - return bytes32(0); - } - - /// @notice Build the 15-account swap instruction meta list per the - /// Meteora dynamic-amm Anchor IDL. - /// - /// Historical note: a trailing 16th meta (`prog_dynamic_amm`) used to be - /// appended here as a Rome-EVM emulator workaround for the pre-#368 - /// `ix_store` filter in `the Rome EVM program`, which - /// dropped `ix.program_id` from the per-CPI store and prevented - /// `mollusk.rs::load_elf` from finding the AMM program. the Rome EVM program - /// PR #368 (the tightened allowlist that explicitly includes - /// `ix.program_id`) made the workaround redundant. - /// - /// Verified empirically on 2026-05-18 via `simulateTransaction` against - /// Solana devnet: with the trailing entry, both real Solana and Mollusk - /// fire `'s writable privilege escalated` during the inner CPI to - /// dynamic-vault (the swap aborts at ~32.7K CU before tokens move). - /// Without the trailing entry, the same swap completes Deposit (vault A) - /// + Withdraw (vault B) cleanly at ~99.5K CU. - function build_swap_account_metas( - SwapAccountsInput memory a - ) - internal - pure - returns (ICrossProgramInvocation.AccountMeta[] memory metas) - { - metas = new ICrossProgramInvocation.AccountMeta[](15); - - metas[0] = ICrossProgramInvocation.AccountMeta(a.pool, false, true); - metas[1] = ICrossProgramInvocation.AccountMeta( - a.user_source_token, - false, - true - ); - metas[2] = ICrossProgramInvocation.AccountMeta( - a.user_destination_token, - false, - true - ); - metas[3] = ICrossProgramInvocation.AccountMeta(a.a_vault, false, true); - metas[4] = ICrossProgramInvocation.AccountMeta(a.b_vault, false, true); - metas[5] = ICrossProgramInvocation.AccountMeta( - a.a_token_vault, - false, - true - ); - metas[6] = ICrossProgramInvocation.AccountMeta( - a.b_token_vault, - false, - true - ); - metas[7] = ICrossProgramInvocation.AccountMeta( - a.a_vault_lp_mint, - false, - true - ); - metas[8] = ICrossProgramInvocation.AccountMeta( - a.b_vault_lp_mint, - false, - true - ); - metas[9] = ICrossProgramInvocation.AccountMeta(a.a_vault_lp, false, true); - metas[10] = ICrossProgramInvocation.AccountMeta(a.b_vault_lp, false, true); - metas[11] = ICrossProgramInvocation.AccountMeta( - a.protocol_token_fee, - false, - true - ); - metas[12] = ICrossProgramInvocation.AccountMeta(a.user, true, false); - metas[13] = ICrossProgramInvocation.AccountMeta( - a.vault_program, - false, - false - ); - metas[14] = ICrossProgramInvocation.AccountMeta( - a.token_program, - false, - false - ); - } - - function build_swap_ix_data(uint64 in_amount, uint64 minimum_out_amount) - internal - pure - returns (bytes memory) - { - return abi.encodePacked(SWAP_PREFIX, Convert.u64le(in_amount), Convert.u64le(minimum_out_amount)); - } - - /// @notice Build the 16-account balance-liquidity (add/remove) meta list - /// per the Meteora dynamic-amm Anchor IDL. - /// - /// Historical note: same trailing-meta workaround as - /// `build_swap_account_metas` (drop it for the same reasons — see comment - /// above). Both swap and balance-liquidity paths go through dynamic-amm - /// → dynamic-vault inner CPIs that trigger the same writable privilege - /// escalation when the trailing entry is present. - function build_balance_liquidity_account_metas( - BalanceLiquidityAccountsInput memory a - ) - internal - pure - returns (ICrossProgramInvocation.AccountMeta[] memory metas) - { - metas = new ICrossProgramInvocation.AccountMeta[](16); - - metas[0] = ICrossProgramInvocation.AccountMeta(a.pool, false, true); - metas[1] = ICrossProgramInvocation.AccountMeta(a.lp_mint, false, true); - metas[2] = ICrossProgramInvocation.AccountMeta(a.user_pool_lp, false, true); - metas[3] = ICrossProgramInvocation.AccountMeta(a.a_vault_lp, false, true); - metas[4] = ICrossProgramInvocation.AccountMeta(a.b_vault_lp, false, true); - metas[5] = ICrossProgramInvocation.AccountMeta(a.a_vault, false, true); - metas[6] = ICrossProgramInvocation.AccountMeta(a.b_vault, false, true); - metas[7] = ICrossProgramInvocation.AccountMeta(a.a_vault_lp_mint, false, true); - metas[8] = ICrossProgramInvocation.AccountMeta(a.b_vault_lp_mint, false, true); - metas[9] = ICrossProgramInvocation.AccountMeta(a.a_token_vault, false, true); - metas[10] = ICrossProgramInvocation.AccountMeta(a.b_token_vault, false, true); - metas[11] = ICrossProgramInvocation.AccountMeta(a.user_a_token, false, true); - metas[12] = ICrossProgramInvocation.AccountMeta(a.user_b_token, false, true); - metas[13] = ICrossProgramInvocation.AccountMeta(a.user, true, false); - metas[14] = ICrossProgramInvocation.AccountMeta(a.vault_program, false, false); - metas[15] = ICrossProgramInvocation.AccountMeta(a.token_program, false, false); - } - - function build_add_balance_liquidity_ix_data( - uint64 pool_token_amount, - uint64 maximum_token_a_amount, - uint64 maximum_token_b_amount - ) - internal - pure - returns (bytes memory) - { - return abi.encodePacked( - ADD_BALANCE_LIQUIDITY_PREFIX, - Convert.u64le(pool_token_amount), - Convert.u64le(maximum_token_a_amount), - Convert.u64le(maximum_token_b_amount) - ); - } - - function build_remove_balance_liquidity_ix_data( - uint64 pool_token_amount, - uint64 minimum_a_token_out, - uint64 minimum_b_token_out - ) - internal - pure - returns (bytes memory) - { - return abi.encodePacked( - REMOVE_BALANCE_LIQUIDITY_PREFIX, - Convert.u64le(pool_token_amount), - Convert.u64le(minimum_a_token_out), - Convert.u64le(minimum_b_token_out) - ); - } -} - -contract DAMMv1Pool { - uint128 internal constant LOCKED_PROFIT_DEGRADATION_DENOMINATOR = - 1_000_000_000_000; - - enum PoolToken { - TokenA, - TokenB - } - - struct Reserves { - uint128 a_reserve; - uint128 b_reserve; - } - - error ZeroReserve(); - error DivisionByZero(); - error MathOverflow(); - error InvalidCurrentTime(uint64 current_time, uint64 last_report); - - bool public initialized; - - address public immutable pool_factory; - address public token_factory; - bytes32 public pool_address; - bytes32 public prog_dynamic_vault; - bytes32 public prog_dynamic_amm; - address public cpi_program; - - ////////////////////////////////////////////////////// - // Pool state (does not fit into stack as a structure) - bytes32 public lp_mint; - bytes32 public token_a_mint; - bytes32 public token_b_mint; - bytes32 public a_vault; - bytes32 public b_vault; - bytes32 public a_vault_lp; - bytes32 public b_vault_lp; - uint8 public a_vault_lp_bump; - bool public enabled; - bytes32 public protocol_token_a_fee; - bytes32 public protocol_token_b_fee; - uint64 public fee_last_updated_at; - DAMMv1Lib.PoolFees public fees; - DAMMv1Lib.PoolType public pool_type; - ////////////////////////////////////////////////////// - - DAMMv1Lib.VaultState public vault_a; - DAMMv1Lib.VaultState public vault_b; - - constructor() { - initialized = false; - pool_factory = msg.sender; - } - - modifier onlyFactory() { - require(msg.sender == pool_factory, "FORBIDDEN"); - _; - } - - function initialize( - address _token_factory, - bytes32 _pool_address, - bytes32 _prog_dynamic_vault, - bytes32 _prog_dynamic_amm, - address _cpi_program - ) public onlyFactory { - require(!initialized, "ALREADY_INITIALIZED"); - initialized = true; - - token_factory = _token_factory; - pool_address = _pool_address; - prog_dynamic_vault = _prog_dynamic_vault; - prog_dynamic_amm = _prog_dynamic_amm; - cpi_program = _cpi_program; - update_state(); - } - - function update_state() public { - DAMMv1Lib.PoolState memory pool = DAMMv1Lib.load_pool(pool_address, cpi_program); - update_pool_state(pool); - vault_a = DAMMv1Lib.load_vault(a_vault, cpi_program); - vault_b = DAMMv1Lib.load_vault(b_vault, cpi_program); - } - - function update_pool_state(DAMMv1Lib.PoolState memory pool) internal - { - lp_mint = pool.lp_mint; - token_a_mint = pool.token_a_mint; - token_b_mint = pool.token_b_mint; - a_vault = pool.a_vault; - b_vault = pool.b_vault; - a_vault_lp = pool.a_vault_lp; - b_vault_lp = pool.b_vault_lp; - a_vault_lp_bump = pool.a_vault_lp_bump; - enabled = pool.enabled; - protocol_token_a_fee = pool.protocol_token_a_fee; - protocol_token_b_fee = pool.protocol_token_b_fee; - fee_last_updated_at = pool.fee_last_updated_at; - fees = pool.fees; - pool_type = pool.pool_type; - } - - function calculate_locked_profit( - DAMMv1Lib.LockedProfitTracker memory tracker, - uint64 current_time - ) - public - pure - returns (uint64) - { - if (current_time < tracker.last_report) { - revert InvalidCurrentTime(current_time, tracker.last_report); - } - - uint128 duration = uint128(current_time - tracker.last_report); - uint128 locked_fund_ratio = duration - * uint128(tracker.locked_profit_degradation); - - if (locked_fund_ratio > LOCKED_PROFIT_DEGRADATION_DENOMINATOR) { - return 0; - } - - uint128 locked_profit = uint128(tracker.last_updated_locked_profit) - * (LOCKED_PROFIT_DEGRADATION_DENOMINATOR - locked_fund_ratio) - / LOCKED_PROFIT_DEGRADATION_DENOMINATOR; - - if (locked_profit > type(uint64).max) { - revert MathOverflow(); - } - - return uint64(locked_profit); - } - - function get_unlocked_amount( - DAMMv1Lib.VaultState memory vault_state, - uint64 current_time - ) - public - pure - returns (uint64) - { - uint64 locked_profit = calculate_locked_profit( - vault_state.locked_profit_tracker, - current_time - ); - - if (locked_profit > vault_state.total_amount) { - revert MathOverflow(); - } - - return vault_state.total_amount - locked_profit; - } - - function get_amount_by_share( - DAMMv1Lib.VaultState memory vault_state, - uint64 current_time, - uint64 share, - uint64 total_supply - ) - public - pure - returns (uint64) - { - if (total_supply == 0) { - revert DivisionByZero(); - } - - uint64 total_amount = get_unlocked_amount(vault_state, current_time); - uint128 amount = uint128(share) * uint128(total_amount) - / uint128(total_supply); - - if (amount > type(uint64).max) { - revert MathOverflow(); - } - - return uint64(amount); - } - - function get_unmint_amount( - DAMMv1Lib.VaultState memory vault_state, - uint64 current_time, - uint64 out_token, - uint64 total_supply - ) - public - pure - returns (uint64) - { - uint64 total_amount = get_unlocked_amount(vault_state, current_time); - if (total_amount == 0) { - revert DivisionByZero(); - } - - uint128 amount = uint128(out_token) * uint128(total_supply) - / uint128(total_amount); - - if (amount > type(uint64).max) { - revert MathOverflow(); - } - - return uint64(amount); - } - - function get_pool_token_amounts() - public - view - returns (uint64 token_a_amount, uint64 token_b_amount) - { - DAMMv1Lib.VaultState memory vault_a_state = DAMMv1Lib.load_vault(a_vault, cpi_program); - DAMMv1Lib.VaultState memory vault_b_state = DAMMv1Lib.load_vault(b_vault, cpi_program); - - // Read only the fields we actually use — `supply` from each LP mint - // (offset 36 in SPL Mint layout) and `amount` from each vault LP token - // account (offset 64 in SPL TokenAccount layout). Typed `account_u64_at` - // (selector 0xb317d4c1) skips the full account_info 6-tuple decode + - // 82/165-byte data buffer fetch that SplTokenLib.load_mint/load_token_amount - // were doing. Saves ~22K CU per typed read; 4 reads per call → ~88K CU - // per get_price_e18 (Agent 3 audit measurement, 2026-05-16). - uint64 a_lp_supply = AccountReader.readU64At(vault_a_state.lp_mint, 36); - uint64 b_lp_supply = AccountReader.readU64At(vault_b_state.lp_mint, 36); - uint64 pool_lp_a = AccountReader.readU64At(a_vault_lp, 64); - uint64 pool_lp_b = AccountReader.readU64At(b_vault_lp, 64); - uint64 current_time = uint64(block.timestamp); - - token_a_amount = a_lp_supply == 0 - ? 0 - : get_amount_by_share(vault_a_state, current_time, pool_lp_a, a_lp_supply); - - token_b_amount = b_lp_supply == 0 - ? 0 - : get_amount_by_share(vault_b_state, current_time, pool_lp_b, b_lp_supply); - } - - function get_reserves() - public - view - returns (Reserves memory r) - { - (uint64 token_a_amount, uint64 token_b_amount) = get_pool_token_amounts(); - r = Reserves({ - a_reserve: uint128(token_a_amount), - b_reserve: uint128(token_b_amount) - }); - } - - function get_price_e18(PoolToken token) - external - view - returns (uint256) - { - Reserves memory r = get_reserves(); - // Read only mint.decimals (offset 44 in SPL Mint layout — 1 byte). - // Typed account_data_at slice (1 byte) vs prior full account_info - // 6-tuple decode. Saves ~20-30K CU per get_price_e18 call. - uint8 dec_a = uint8(AccountReader.readBytesAt(token_a_mint, 44, 1)[0]); - uint8 dec_b = uint8(AccountReader.readBytesAt(token_b_mint, 44, 1)[0]); - - if (token == PoolToken.TokenA) { - if (r.a_reserve == 0) revert ZeroReserve(); - - return uint256(r.b_reserve) - * (10 ** uint256(dec_a)) - * 1e18 - / uint256(r.a_reserve) - / (10 ** uint256(dec_b)); - } else { - if (r.b_reserve == 0) revert ZeroReserve(); - - return uint256(r.a_reserve) - * (10 ** uint256(dec_b)) - * 1e18 - / uint256(r.b_reserve) - / (10 ** uint256(dec_a)); - } - } - - function get_fees_e18() - external - view - returns (uint256) - { - if ( - fees.trade_fee_denominator == 0 - || fees.protocol_trade_fee_denominator == 0 - ) { - revert DivisionByZero(); - } - - uint256 lp_fee_e18 = uint256(fees.trade_fee_numerator) * 1e18 - / uint256(fees.trade_fee_denominator); - - uint256 protocol_fee_e18 = - uint256(fees.protocol_trade_fee_numerator) * 1e18 - / uint256(fees.protocol_trade_fee_denominator); - - return lp_fee_e18 * (1e18 + protocol_fee_e18) / 1e18; - } - - /// @notice Build the swap-instruction account list for a user. - /// @param user_addr EVM address of the swapping user. PDA + ATAs are - /// derived server-side by the HelperProgram precompile in a - /// single dispatch each, saving ~64K CU per ATA call vs the - /// legacy two-hop pattern. See: - /// the Rome design specs - function make_swap_accounts_from_pool( - address user_addr, - PoolToken in_token - ) public view returns (DAMMv1Lib.SwapAccountsInput memory out) { - bytes32 protocol_token_fee = in_token == PoolToken.TokenA - ? protocol_token_a_fee - : protocol_token_b_fee; - - (bytes32 in_token_mint, bytes32 out_token_mint) = in_token == PoolToken.TokenA - ? (token_a_mint, token_b_mint) - : (token_b_mint, token_a_mint); - - out = DAMMv1Lib.SwapAccountsInput({ - pool: pool_address, - user_source_token: HelperProgram.ata(user_addr, in_token_mint), - user_destination_token: HelperProgram.ata(user_addr, out_token_mint), - a_vault_lp: a_vault_lp, - b_vault_lp: b_vault_lp, - a_vault: a_vault, - b_vault: b_vault, - a_vault_lp_mint: vault_a.lp_mint, - b_vault_lp_mint: vault_b.lp_mint, - a_token_vault: vault_a.token_vault, - b_token_vault: vault_b.token_vault, - user: HelperProgram.pda(user_addr), - vault_program: prog_dynamic_vault, - token_program: SplTokenLib.SPL_TOKEN_PROGRAM, - protocol_token_fee: protocol_token_fee - }); - } - - /// @notice Build the add/remove-liquidity instruction account list. - /// @param user_addr EVM address of the LP user. PDA + ATAs derived - /// server-side via HelperProgram. Three ATAs total (pool LP, - /// token_a, token_b) — saves ~192K CU per liquidity op vs - /// the legacy two-hop pattern. - function make_balance_liquidity_accounts_from_pool( - address user_addr - ) - public - view - returns (DAMMv1Lib.BalanceLiquidityAccountsInput memory out) - { - out = DAMMv1Lib.BalanceLiquidityAccountsInput({ - pool: pool_address, - lp_mint: lp_mint, - user_pool_lp: HelperProgram.ata(user_addr, lp_mint), - a_vault_lp: a_vault_lp, - b_vault_lp: b_vault_lp, - a_vault: a_vault, - b_vault: b_vault, - a_vault_lp_mint: vault_a.lp_mint, - b_vault_lp_mint: vault_b.lp_mint, - a_token_vault: vault_a.token_vault, - b_token_vault: vault_b.token_vault, - user_a_token: HelperProgram.ata(user_addr, token_a_mint), - user_b_token: HelperProgram.ata(user_addr, token_b_mint), - user: HelperProgram.pda(user_addr), - vault_program: prog_dynamic_vault, - token_program: SplTokenLib.SPL_TOKEN_PROGRAM - }); - } - - /// @notice Variant of `make_balance_liquidity_accounts_from_pool` - /// where user_accounts are pre-derived (no ATA derivation here). - /// @param user_addr EVM address — PDA derived via HelperProgram for - /// the `accounts_.user` AccountMeta only. - function make_balance_liquidity_accounts_from_pool_and_user_accounts( - address user_addr, - DAMMv1Lib.AddLiquidityUserAccountsInput memory user_accounts - ) - public - view - returns (DAMMv1Lib.BalanceLiquidityAccountsInput memory out) - { - out = DAMMv1Lib.BalanceLiquidityAccountsInput({ - pool: pool_address, - lp_mint: lp_mint, - user_pool_lp: user_accounts.user_pool_lp, - a_vault_lp: a_vault_lp, - b_vault_lp: b_vault_lp, - a_vault: a_vault, - b_vault: b_vault, - a_vault_lp_mint: vault_a.lp_mint, - b_vault_lp_mint: vault_b.lp_mint, - a_token_vault: vault_a.token_vault, - b_token_vault: vault_b.token_vault, - user_a_token: user_accounts.user_a_token, - user_b_token: user_accounts.user_b_token, - user: HelperProgram.pda(user_addr), - vault_program: prog_dynamic_vault, - token_program: SplTokenLib.SPL_TOKEN_PROGRAM - }); - } -} - -contract ERC20DAMMv1Pool { - DAMMv1Pool public immutable internal_pool; - ERC20Users private immutable users; - SPL_ERC20 private immutable tokenA; - SPL_ERC20 private immutable tokenB; - - constructor( - DAMMv1Pool _internal_pool, - ERC20SPLFactory token_factory - ) { - address toka_a_address = token_factory.token_by_mint(_internal_pool.token_a_mint()); - require(toka_a_address != address(0), "Token A mint does not exist in factory"); - address toka_b_address = token_factory.token_by_mint(_internal_pool.token_b_mint()); - require(toka_b_address != address(0), "Token B mint does not exist in factory"); - - tokenA = SPL_ERC20(toka_a_address); - tokenB = SPL_ERC20(toka_b_address); - internal_pool = _internal_pool; - users = token_factory.users(); - } - - function update_state() external { - internal_pool.update_state(); - } - - function get_reserves() external view returns (DAMMv1Pool.Reserves memory) { - return internal_pool.get_reserves(); - } - - function get_price_e18(DAMMv1Pool.PoolToken token) - external - view - returns (uint256) { - return internal_pool.get_price_e18(token); - } - - function get_fees_e18() external view returns (uint256) { - return internal_pool.get_fees_e18(); - } - - function quoteAddLiquidity( - uint256 max_token_a_amount, - uint256 max_token_b_amount, - uint256 slippage_rate - ) - public - view - returns (uint64) - { - require(max_token_a_amount <= type(uint64).max, "max_token_a_amount exceeds uint64"); - require(max_token_b_amount <= type(uint64).max, "max_token_b_amount exceeds uint64"); - require(slippage_rate <= type(uint64).max, "slippage_rate exceeds uint64"); - require(slippage_rate <= 100, "slippage_rate exceeds 100"); - - (uint64 token_a_amount, uint64 token_b_amount) = internal_pool.get_pool_token_amounts(); - require(token_a_amount != 0, "token_a_amount is zero"); - require(token_b_amount != 0, "token_b_amount is zero"); - - SplTokenLib.SplMint memory pool_lp_mint = SplTokenLib.load_mint( - internal_pool.lp_mint(), - internal_pool.cpi_program() - ); - - uint256 pool_token_by_a = max_token_a_amount - * uint256(pool_lp_mint.supply) - / uint256(token_a_amount); - - uint256 pool_token_by_b = max_token_b_amount - * uint256(pool_lp_mint.supply) - / uint256(token_b_amount); - - uint256 pool_token_amount = _min(pool_token_by_a, pool_token_by_b) - * (100 - slippage_rate) - / 100; - - require(pool_token_amount <= type(uint64).max, "pool_token_amount exceeds uint64"); - return uint64(pool_token_amount); - } - - function prepareAddLiquidity( - address user_evm, - uint256 max_token_a_amount, - uint256 max_token_b_amount, - uint256 slippage_rate - ) - external - view - returns ( - uint64 pool_token_amount, - DAMMv1Lib.BalanceLiquidityAccountsInput memory liquidity_accounts - ) - { - // `users.get_user` reverts if `user_evm` isn't registered — keep - // as a precondition check. Return value no longer needed by the - // downstream make_* helper (it now takes the EVM address and - // derives the PDA via HelperProgram.pda internally). - users.get_user(user_evm); - - pool_token_amount = quoteAddLiquidity( - max_token_a_amount, - max_token_b_amount, - slippage_rate - ); - liquidity_accounts = internal_pool.make_balance_liquidity_accounts_from_pool(user_evm); - } - - function swapExactTokensForTokens( - address token_in, - uint256 amount_in, - uint256 min_amount_out - ) external { - // Precondition: caller must be registered. Return value unused - // post-migration; the make_* helper derives PDA + ATAs from the - // EVM address via HelperProgram (saves ~128K CU per swap). - users.get_user(msg.sender); - - DAMMv1Pool.PoolToken in_token = - address(tokenA) == token_in - ? DAMMv1Pool.PoolToken.TokenA - : DAMMv1Pool.PoolToken.TokenB; - - require(amount_in <= type(uint64).max, "amount_in exceeds uint64"); - require(min_amount_out <= type(uint64).max, "min_amount_out exceeds uint64"); - - ICrossProgramInvocation.AccountMeta[] memory accounts = DAMMv1Lib.build_swap_account_metas( - internal_pool.make_swap_accounts_from_pool( - msg.sender, - in_token - ) - ); - - bytes memory data = DAMMv1Lib.build_swap_ix_data(uint64(amount_in), uint64(min_amount_out)); - bytes32[] memory seeds = new bytes32[](0); - - (bool success, bytes memory result) = address(internal_pool.cpi_program()).delegatecall( - abi.encodeWithSelector( - ICrossProgramInvocation.invoke_signed.selector, - internal_pool.prog_dynamic_amm(), - accounts, - data, - seeds - ) - ); - - require(success, string(result)); - } - - function removeLiquidity( - uint256 pool_token_amount, - uint256 minimum_a_token_out, - uint256 minimum_b_token_out - ) external { - // Precondition: caller registered. Return value unused - // post-migration (saves ~192K CU per remove-liquidity). - users.get_user(msg.sender); - - require(pool_token_amount <= type(uint64).max, "pool_token_amount exceeds uint64"); - require(minimum_a_token_out <= type(uint64).max, "minimum_a_token_out exceeds uint64"); - require(minimum_b_token_out <= type(uint64).max, "minimum_b_token_out exceeds uint64"); - - ICrossProgramInvocation.AccountMeta[] memory accounts = - DAMMv1Lib.build_balance_liquidity_account_metas( - internal_pool.make_balance_liquidity_accounts_from_pool(msg.sender) - ); - - bytes memory data = DAMMv1Lib.build_remove_balance_liquidity_ix_data( - uint64(pool_token_amount), - uint64(minimum_a_token_out), - uint64(minimum_b_token_out) - ); - bytes32[] memory seeds = new bytes32[](0); - - (bool success, bytes memory result) = address(internal_pool.cpi_program()).delegatecall( - abi.encodeWithSelector( - ICrossProgramInvocation.invoke_signed.selector, - internal_pool.prog_dynamic_amm(), - accounts, - data, - seeds - ) - ); - - require(success, string(result)); - } - - function addLiquidity( - uint256 pool_token_amount, - uint256 max_token_a_amount, - uint256 max_token_b_amount, - DAMMv1Lib.AddLiquidityUserAccountsInput memory user_accounts - ) external { - // Precondition: caller registered. Return value unused - // post-migration (saves ~67K CU for the user-PDA derivation). - users.get_user(msg.sender); - - require(pool_token_amount <= type(uint64).max, "pool_token_amount exceeds uint64"); - require(max_token_a_amount <= type(uint64).max, "max_token_a_amount exceeds uint64"); - require(max_token_b_amount <= type(uint64).max, "max_token_b_amount exceeds uint64"); - - ICrossProgramInvocation.AccountMeta[] memory accounts = - DAMMv1Lib.build_balance_liquidity_account_metas( - internal_pool.make_balance_liquidity_accounts_from_pool_and_user_accounts( - msg.sender, - user_accounts - ) - ); - - bytes memory data = DAMMv1Lib.build_add_balance_liquidity_ix_data( - uint64(pool_token_amount), - uint64(max_token_a_amount), - uint64(max_token_b_amount) - ); - bytes32[] memory seeds = new bytes32[](0); - - (bool success, bytes memory result) = address(internal_pool.cpi_program()).delegatecall( - abi.encodeWithSelector( - ICrossProgramInvocation.invoke_signed.selector, - internal_pool.prog_dynamic_amm(), - accounts, - data, - seeds - ) - ); - - require(success, string(result)); - } - - function ensurePoolLpTokenAccount() external returns (bytes32) { - bytes32 user = users.get_user(msg.sender); - return _createAssociatedTokenAccountIdempotent( - user, - user, - internal_pool.lp_mint() - ); - } - - function _createAssociatedTokenAccountIdempotent( - bytes32 funding_address, - bytes32 wallet_address, - bytes32 token_mint_address - ) internal returns (bytes32 associated_account_address) { - ( - bytes32 program_id, - ICrossProgramInvocation.AccountMeta[] memory accounts, - bytes memory data, - bytes32 associated_account_address - ) = AssociatedSplToken.create_associated_token_account_idempotent( - funding_address, - wallet_address, - token_mint_address, - SystemProgramLib.PROGRAM_ID, - SplTokenLib.SPL_TOKEN_PROGRAM, - AssociatedSplToken.ASSOCIATED_TOKEN_PROGRAM_ID - ); - - bytes32[] memory seeds = new bytes32[](0); - - (bool success, bytes memory result) = address(internal_pool.cpi_program()).delegatecall( - abi.encodeWithSelector( - ICrossProgramInvocation.invoke_signed.selector, - program_id, - accounts, - data, - seeds - ) - ); - - require(success, string(result)); - return associated_account_address; - } - - function _min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } -} diff --git a/contracts/meteora/damm_v1_router.sol b/contracts/meteora/damm_v1_router.sol deleted file mode 100644 index e42cf0a..0000000 --- a/contracts/meteora/damm_v1_router.sol +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {MeteoraDAMMv1Factory} from "./damm_v1_factory.sol"; -import {DAMMv1Lib, DAMMv1Pool, ERC20DAMMv1Pool} from "./damm_v1_pool.sol"; -import {SPL_ERC20} from "../erc20spl/erc20spl.sol"; -import {Convert} from "../convert.sol"; -import {ICrossProgramInvocation} from "../interface.sol"; -import {EnsureAta} from "../cpi/EnsureAta.sol"; - -contract MeteoraDAMMv1Router { - MeteoraDAMMv1Factory public immutable factory; - address public cpi_program; - - constructor(MeteoraDAMMv1Factory _factory, address _cpi_program) { - factory = _factory; - cpi_program = _cpi_program; - } - - function swapExactTokensForTokens( - address token_in, - address token_out, - uint256 amount_in, - uint256 min_amount_out - ) external { - address pool = factory.getPool(token_in, token_out); - require(pool != address(0), "Pool does not exist"); - - // Pre-flight: idempotently ensure user's ATAs exist for both sides. - // Meteora's swap declares `Account<'info, TokenAccount>` constraints - // on user_source_token / user_destination_token; Anchor rejects with - // Custom(3012) AccountNotInitialized before the handler runs if - // either is missing. Composing the create-idempotent calls in the - // same EVM tx keeps the swap atomic at the user-visible layer. - // CU: ~50K per side when ATA exists (idempotent no-op CPI), ~110K - // when creating. Two sides = 100-220K total; well under the 1.4M - // atomic envelope alongside the swap (~700-900K typical). - EnsureAta.ensure(msg.sender, SPL_ERC20(token_in).mint_id()); - EnsureAta.ensure(msg.sender, SPL_ERC20(token_out).mint_id()); - - (bool success, bytes memory result) = pool.delegatecall( - abi.encodeWithSelector( - ERC20DAMMv1Pool.swapExactTokensForTokens.selector, - token_in, amount_in, min_amount_out - ) - ); - - require (success, string(Convert.revert_msg(result))); - } - - function addLiquidity( - address token_a, - address token_b, - uint256 pool_token_amount, - uint256 max_token_a_amount, - uint256 max_token_b_amount, - DAMMv1Lib.AddLiquidityUserAccountsInput memory user_accounts - ) external { - address pool = factory.getPool(token_a, token_b); - require(pool != address(0), "Pool does not exist"); - - (bool success, bytes memory result) = pool.delegatecall( - abi.encodeWithSelector( - ERC20DAMMv1Pool.addLiquidity.selector, - pool_token_amount, - max_token_a_amount, - max_token_b_amount, - user_accounts - ) - ); - - require(success, string(Convert.revert_msg(result))); - } - - function prepareAddLiquidity( - address token_a, - address token_b, - address user_evm, - uint256 max_token_a_amount, - uint256 max_token_b_amount, - uint256 slippage_rate - ) - external - view - returns ( - uint64 pool_token_amount, - DAMMv1Lib.BalanceLiquidityAccountsInput memory liquidity_accounts - ) - { - address pool = factory.getPool(token_a, token_b); - require(pool != address(0), "Pool does not exist"); - - return ERC20DAMMv1Pool(pool).prepareAddLiquidity( - user_evm, - max_token_a_amount, - max_token_b_amount, - slippage_rate - ); - } - - function ensurePoolLpTokenAccount( - address token_a, - address token_b - ) external { - address pool = factory.getPool(token_a, token_b); - require(pool != address(0), "Pool does not exist"); - - (bool success, bytes memory result) = pool.delegatecall( - abi.encodeWithSelector( - ERC20DAMMv1Pool.ensurePoolLpTokenAccount.selector - ) - ); - - require(success, string(Convert.revert_msg(result))); - } - - function removeLiquidity( - address token_a, - address token_b, - uint256 pool_token_amount, - uint256 minimum_a_token_out, - uint256 minimum_b_token_out - ) external { - address pool = factory.getPool(token_a, token_b); - require(pool != address(0), "Pool does not exist"); - - (bool success, bytes memory result) = pool.delegatecall( - abi.encodeWithSelector( - ERC20DAMMv1Pool.removeLiquidity.selector, - pool_token_amount, - minimum_a_token_out, - minimum_b_token_out - ) - ); - - require(success, string(Convert.revert_msg(result))); - } -} diff --git a/contracts/meteora/test/DAMMv1VaultParserHarness.sol b/contracts/meteora/test/DAMMv1VaultParserHarness.sol deleted file mode 100644 index 2a9d3a5..0000000 --- a/contracts/meteora/test/DAMMv1VaultParserHarness.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import "../damm_v1_pool.sol"; - -/// @title DAMMv1VaultParserHarness -/// @notice Test wrapper exposing DAMMv1Lib.parse_vault + VAULT_MIN_LEN as -/// external calls so unit tests can verify the slice constant -/// matches the parser's actual byte consumption. -contract DAMMv1VaultParserHarness { - function parseVault(bytes memory data) external pure returns ( - uint8 enabled, - uint8 vaultBump, - uint8 tokenVaultBump, - uint64 totalAmount, - bytes32 tokenVault, - bytes32 feeVault, - bytes32 tokenMint, - bytes32 lpMint - ) { - DAMMv1Lib.VaultState memory v = DAMMv1Lib.parse_vault(data); - return ( - v.enabled, - v.bumps.vault_bump, - v.bumps.token_vault_bump, - v.total_amount, - v.token_vault, - v.fee_vault, - v.token_mint, - v.lp_mint - ); - } - - function vaultMinLen() external pure returns (uint256) { - return DAMMv1Lib.VAULT_MIN_LEN; - } -} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c6273fe --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,194 @@ +# rome-solidity — architecture & contract reference + +The complete map of what this repo exposes, **why each piece exists**, and **where it's used** across the Rome ecosystem. Ordered by importance — the primitives everything else builds on come first. + +> **Mental model.** Rome runs an EVM *inside* a Solana program. A Solidity contract reaches the surrounding Solana runtime only through **precompiles** at fixed addresses (`0xff…`, `0x42…16`). `contracts/interface.sol` is the ABI for those precompiles; everything else in this repo is a Solidity layer built on top of them — the CPI toolkit, the SPL↔ERC-20 wrappers, the bridge egress, and the oracle adapters. + +**The layers, high to low:** + +``` +examples/ worked references + ├── erc20spl/ bridge/ oracle/ capabilities you consume + │ └── cpi/ the builder-facing CPI toolkit + │ └── interface.sol precompile ABIs (the ground floor) + └── spl_token/ system_program/ rome_evm_account low-level account/token primitives +``` + +--- + +## 1. Precompile interfaces — `contracts/interface.sol` + +The ABI bindings to Rome's non-EVM precompiles. Everything in the repo ultimately calls these. Two families: + +- **CPI / utility family** — `ISystemProgram` (`0xff…07`, reads), `ICrossProgramInvocation` (`0xff…08`, the raw CPI primitive), `IHelperProgram` (`0xff…09`, composite one-shots), `IWithdraw` (`0x42…16`, legacy non-revertable gas exit). +- **Cached family** — `ISystemCached` (`0xff…04`), `ISplCached` (`0xff…05`), `IAssociatedSplCached` (`0xff…06`), `IWithdrawCached` (`0xff…0b`). These route the Solana side effect through the Rome-EVM **journal**, so it is **atomically revertable** alongside EVM state and is **iterative-VM compatible** (multi-step txs). + +| Interface | Addr | What | Why | +|---|---|---|---| +| `ISystemProgram` | `0xff…07` | PDA derivation, base58 codec, chain-identity getters (view/pure) | EVM code must compute Solana addresses + read host identity without leaving the EVM | +| `ICrossProgramInvocation` | `0xff…08` | `invoke` / `invoke_signed` + account-read & batch-PDA shortcuts | *The* primitive that makes "EVM inside Solana" composable — call any Solana program | +| `IHelperProgram` | `0xff…09` | ATA/PDA create, SPL transfer/approve/mint, gas↔lamports, collapsed reads | Folds multi-step CPI compositions into single dispatches to save CU | +| `IWithdraw` | `0x42…16` | Move native gas value out of the EVM to Solana (owner/PDA/ATA) | Users must exit EVM gas balances back to Solana; legacy non-revertable path | +| `ISystemCached` | `0xff…04` | Journaled System-program ops (create_pda/allocate/transfer) | Account ops that revert atomically with the EVM tx | +| `ISplCached` | `0xff…05` | Journaled SPL Token ops (ERC-20-shaped transfer/approve/mint + reads) | Revertable SPL ops; exposes ERC-20 selectors so SPL behaves like ERC-20 | +| `IAssociatedSplCached` | `0xff…06` | Journaled Associated-Token-Account creation | Create ATAs on the revertable path | +| `IWithdrawCached` | `0xff…0b` | Journaled gas exit + `deposit` inverse (SPL in → native gas) | Revertable gas wrap/unwrap | + +**Where used:** `rome-sdk-ts` mirrors the whole surface in TypeScript (`src/abis.ts`, `src/selectors.ts`, `src/addresses.ts`); `compound-on-rome-comet` **vendors** the bindings (`contracts/lib/RomePrecompiles.sol` — "copied from rome-solidity"); `cardo` and `appia` mirror the CPI precompile ABI in TS (`lib/cpi-precompile.ts`). + +--- + +## 2. CPI toolkit — `contracts/cpi/` + +Builder-facing Solidity libraries layered over the CPI precompile — the ergonomic way to call a Solana program from Solidity. Start here: **`contracts/cpi/README.md`** is the "CPI Foundation" guide (three-layer adapter pattern, the `tx.origin` ban, `invoke` vs `invoke_signed`, cost-quote checklist, CU/account budgets). + +| Contract | What | Key surface | +|---|---|---| +| `Cpi` | Canonical thin wrapper over the CPI precompile — one call site for every EVM→Solana CPI | `invoke` / `invokeSigned` / `accountInfo` | +| `AccountMetaBuilder` | Fluent builder for `AccountMeta[]` arrays (signer/writable/readonly) | `alloc` / `signer` / `writable` / `buildChecked` | +| `AnchorInstruction` | Anchor call-data encoding (8-byte discriminator + Borsh LE + `Option`) | `discriminator(name)` / `withDisc` / `u64le` / `optionSome` | +| `AccountReader` | Typed wrappers over the account-read shortcut selectors — **reads, not CPIs** (no signing, no side effect) | `lamportsOf` / `readU64At` / `readBytesAt` | +| `PdaDeriver` | Single-PDA `find_program_address` + typed seed builders | `derive` / `seedBytes` / `makeSeeds` | +| `PdasBatch` | Derive N PDAs against one program in a single syscall (~50–80K CU saved per PDA) | `derive(seedGroups, programId)` / `pair` / `triplet` | +| `UserPda` | EVM address → Solana user PDA + ATA, always via an explicit `address` (never `tx.origin`) | `pda(address)` / `ata(address, mint)` / `atas` | +| `EnsureAta` | Idempotently create a user's ATA before a downstream CPI needs it | `ensure(user, mint)` | +| `SolanaConstants` | Canonical Solana sysvar/program pubkeys as `bytes32` | `SPL_TOKEN_PROGRAM` / `SYSVAR_RENT` / … | +| `CostEstimate` / `CostEstimator` / `ICostView` | Uniform pre-sign USD cost-quote shape + helpers (rent math, existence checks, oracle-priced) | `quoteCost(user, inputs) → CostEstimate` | +| `CpiError` | Shared error selectors for adapters | `AmountTooLarge` / `SignerMismatch` / `InvalidAccountCount` | +| `templates/CpiAdapterBase` | Abstract adapter base — Ownable + Pausable + ReentrancyGuard + rotatable backend | `setBackend` / `pause` / `withdrawERC20` | +| `templates/CpiProgramWrapper` | Prose scaffold for per-adapter golden-vector test wrappers (non-functional) | — | + +**Why it exists:** replaces ~12 inline precompile calls per adapter with one legible, CU-aware, phishing-safe (`address`-explicit) library layer. **Where used:** `cardo` (primary — drives Jupiter/Meteora/Marinade/Streamflow via CPI), `appia`, `compound-on-rome-comet` (vendored precompiles), and `rome-dex`'s EVM-lane router. Public apps typically mirror the CPI precompile ABI in TypeScript rather than importing the Solidity libraries directly. + +--- + +## 3. SPL ↔ ERC-20 wrappers — `contracts/erc20spl/` + +**Any SPL token is an ERC-20 on Rome** through these wrappers — same account, no bridge hop. There are **two wrapper tracks** exposing the identical `IERC20 + IERC20Metadata` surface; they differ only in which precompile family their mutations dispatch through. **This choice is a hard, per-contract rule — a contract uses one track, never both.** + +### The two paths — which to use + +| | **Cached** — `SPL_ERC20_cached` | **CPI-based** — `SPL_ERC20` | +|---|---|---| +| Dispatches through | cached precompiles `SplCached 0xff…05` / `AssociatedSplCached 0xff…06` | `HelperProgram 0xff…09` → real CPI Invoke | +| Side effect | journaled → **commits at end-of-tx, unwinds on EVM revert** | **hits Solana at call time** — not revertable if the EVM tx later reverts | +| Iterative (multi-step) VM | ✅ compatible | ✗ hits `CpiProhibitedInIterativeTx` after the first transfer | +| CU cost | cheaper (2–10%) | higher | +| **Use it for** | **the default** — all standard ERC-20 flows, anything composing in the iterative VM (multi-hop DEX, bulkers), anything needing EVM-revert atomicity | operations the cached track can't do — pushing SPL out to an **arbitrary raw Solana wallet** (`bridgeOutToSolana` / `ensureRecipientAta`, which need the **permanently-CPI-only** `create_ata_for_key`) | + +The factory deploys the **cached** wrapper for every token (`new SPL_ERC20_cached(...)`); the cached header states it *"replaces the CPI-based SPL_ERC20 on devnet."* Reach for the CPI wrapper only when you specifically need its Solana-wallet exit path. + +| Contract | What | Key surface | +|---|---|---| +| `SPL_ERC20_cached` | Cached-track ERC-20 wrapper (default); no CPI Invoke, overlay-aware reads | `transfer`/`transferFrom`/`approve`/`mint_to` → `SplCached` | +| `SPL_ERC20` | CPI-track ERC-20 wrapper; can push SPL to a raw Solana wallet | `bridgeOutToSolana(bytes32,uint256)`, `transfer`/`approve` via `HelperProgram` | +| `ERC20Users` | Shared registry: EVM `address` → its `external_auth` Solana PDA | `ensure_user` / `get_user` | +| `ERC20SPLFactory` | Wrap existing SPL mints (with/without metadata) + mint brand-new SPL tokens; deploys the cached wrapper | `add_spl_token_with_metadata` / `add_spl_token_no_metadata` / `create_token_mint` | +| `cached_revert_demo` | Executable proof of the cached track's revert / iterative / one-track properties | (demos) | + +**Where used:** the AMM & lending forks trade against the **cached** wrappers — Uniswap-style AMM forks, `rome-aave-v3` (reserves = wUSDC/wETH/wSOL cached), `compound-on-rome-comet` (collateral/base, wired by address); UIs `aerarium`, `cardo`, `rome-aave-v3-demo` read/transfer them; `ERC20SPLFactory` is deployed/reused by the AMM forks and `cardo`. The non-cached `SPL_ERC20` has no public app consumer by name — it's superseded by the cached track on devnet and retained for its CPI-only exit path. + +--- + +## 4. Bridge — `contracts/bridge/` + +The **on-chain half** of the Rome bridge — the *egress* surface for leaving Rome. (The off-chain orchestrator that fetches CCTP attestations / Wormhole VAAs and submits the destination leg is the separate **`rome-bridge-api`** repo.) + +| Contract | What | Key surface | +|---|---|---| +| `RomeBridgeWithdraw` | The single outbound entrypoint — takes an SPL-wrapper token in on Rome and fires a Solana CPI (signed as the caller's Rome PDA) over one of five rails | see rails below | +| `RomeBridgeEvents` | Shared event schema the `rome-bridge-api` indexer watches to attribute each egress | `Withdrawn` / `WormholeBurn` / `BridgedOutToSolana` / … | +| `ICCTPV2` (`CCTPV2Lib`) | **The live CCTP encoder** — `deposit_for_burn` bytes + 19 account-metas for Circle CCTP **v2** | `encodeDepositForBurn` / `buildDepositForBurnAccounts` | +| `ICCTP` (`CCTPLib`) | Legacy CCTP **v1** encoder (retained reference; not imported by the live withdraw contract) | `encodeDepositForBurn` (v1, 18 metas) | +| `IWormholeTokenBridge` | Wormhole egress encoders — `transfer_wrapped` (tag 4) and `transfer_native` (tag 5) | `encodeTransferTokens` / `buildTransferWrappedAccounts` | + +**The five egress rails** on `RomeBridgeWithdraw` (all permissionless; network config injected via constructor, so one bytecode works on any chain): +1. **CCTP (USDC)** — `burnUSDC(amount, recipient[, destinationDomain])` → CCTP **v2** `deposit_for_burn`. Per-call Circle domain (e.g. Monad = domain 15, v2-only). +2. **Wormhole ETH** — `approveBurnETH` then `burnETH` (two txs — a CPI forces atomic mode, and two CPIs won't fit one Solana tx's ~1.4M CU). +3. **Generic Wormhole** — `approveWormholeBurn` then `burnToWormhole(assetWrapper, amount, recipient, targetChain)` for any allowlisted wrapper/chain. +4. **Wormhole native** — `transferNativeToWormhole(...)` locks Solana-native mints (wSOL/LSTs) into per-mint custody instead of burning. +5. **Rome → Solana SPL** — `bridgeOutToSolana(recipient, amount, mint)` + `ensureRecipientAta(...)` (the generic SPL exit for any wrapper). + +**Why it exists:** the trustless, user-signed on-chain path out of Rome; the user's single `external_auth` PDA is auto-signed by the rome-evm precompile (users pre-fund it via `SimpleActivator`). **Where used:** `rome-bridge-api` is the canonical consumer (builds egress txns for all five rails); `appia` (`src/egress.ts` — "deliver to my wallet" Wormhole egress), `cardo` (bridge-out flows), `rome-aave-v3-demo` (cross-chain funding UI). + +> Note: `contracts/bridge/README.md` is Phase-1-era and lags the code (documents CCTP v1, omits v2/Monad + `burnToWormhole` + `transferNativeToWormhole` + the unified-PDA model). Trust `RomeBridgeWithdraw.sol`'s NatSpec and this document over it. + +--- + +## 5. Oracle — `contracts/oracle/` + +Solana price feeds (Pyth Pull, Switchboard V2) surfaced to EVM contracts through the **standard Chainlink `AggregatorV3Interface`** — so a Compound/Aave-style consumer reads them unmodified. Builder-facing home: **`rome-oracle-gateway`** (a pointer repo; the contracts live here). A deploy portal + a refresh keeper (run by Rome) deploy the adapter clones and keep the cached feeds fresh. + +| Contract | What | +|---|---| +| `IAggregatorV3Interface` | The verbatim Chainlink interface every adapter implements — the common denominator | +| `IExtendedOracleAdapter` | `is IAggregatorV3Interface` + confidence/EMA/status/source reads | +| `PythPullAdapter` | **Direct** adapter — reads a Pyth `PriceUpdateV2` account off Solana on every read, normalizes to 8 decimals | +| `SwitchboardV3Adapter` | **Direct** adapter over Switchboard V2 (keeps "V3" name for ABI back-compat; `latestEMAData` reverts) | +| `CachedPythAdapter` | **Cached** Pyth adapter — `refresh()` does the ~509K-CU parse+SSTORE on a keeper tx; `latestRoundData()` is a cheap SLOAD | +| `CachedFeedAdapter` | **Cached** decorator over *any* `AggregatorV3` feed (Pyth/Switchboard/Chainlink) | +| `PythPullParser` / `SwitchboardParser` | Borsh decoders pinning the validated Solana account byte-layouts | +| `OracleAdapterFactory` | Deploys all four adapter kinds as EIP-1167 clones; validates the Solana account's owner-program; holds the pause kill-switch | +| `BatchReader` | Fans out reads/health over many feeds with per-feed `try/catch` isolation | +| `IAdapterFactory` / `IAdapterMetadata` | Pause-check callback + one-call self-description | +| `examples/MockChainlinkOracle` / `SampleLendingOracle` | A settable mock feed + a reference consumer proving the Chainlink abstraction holds | + +**Why cached vs direct:** a multi-collateral borrow that reads every feed atomically blows the ~1.4M-CU cap on the ~509K-CU-per-read Pyth parse; the cached adapters move the parse onto the keeper's own tx (trust unchanged — they snapshot the *verified* on-chain account). **Where used:** `rome-aave-v3` (AaveOracle reads adapters via `IAggregatorV3Interface`), `compound-on-rome-comet` (Chainlink-compatible feeds), `rome-dex` + `cardo` + `aerarium` (live price reads); the oracle portal + keeper deploy clones and refresh the cached feeds. Adapter addresses live in the registry (`chains//oracle.json`). + +--- + +## 6. Account & token primitives + +Low-level building blocks the layers above rest on. + +| Contract | What | Why | +|---|---|---| +| `rome_evm_account.sol` (`RomeEVMAccount`) | Derives a user's unified `external_auth` PDA (+ salted variants); computes SPL rent minimums | Every wrapper/factory/bridge resolves "the Solana account that acts for this EVM address" here | +| `activation/SimpleActivator.sol` | One-tx **user-paid** bootstrap: `activate{value}()` creates+funds the caller's PDA + wUSDC/wSOL ATAs + registers the user (~234K CU) | A fresh EVM address becomes fully transactable in one MetaMask popup, Sybil-resistant | +| `wrap/WrappedGasFacade.sol` | WETH9-shaped `deposit()`/`withdraw()` over the native-gas-wrapper precompiles | Explorers/indexers see gas wrap/unwrap as ordinary WETH9 events. Cached-track only | +| `spl_token/spl_token.sol` (`SplTokenLib`) | Pure Borsh decoder for the SPL `Mint` (82-byte) + token-account `amount` layouts | Wrappers need typed decimals/supply/amount out of raw Solana bytes | +| `spl_token/associated_spl_token.sol` | Builds the Associated-Token `Create`/`CreateIdempotent` ix + derives the ATA address | The deterministic ATA-address primitive | +| `system_program/system_program.sol` | Pure builder for the Solana System `transfer` (lamports) instruction | Lamport moves (e.g. funding a PDA payer) need a correctly LE-encoded ix | +| `convert.sol` (`Convert`) | LE/Borsh readers + writers + revert-reason decoder | Solana is LE/Borsh, the EVM is big-endian — every parse/serialize goes through here | + +**Where used:** internal to the wrappers/bridge/factory; `RomeEVMAccount.pda` derivation is mirrored by `rome-sdk-ts` (`src/pda.ts`); `SimpleActivator.activate` is the funding step every dual-lane app calls before a user's first PDA-signed action. + +--- + +## 7. Examples — `contracts/examples/` + +Worked, compilable references for the toolkit: + +| File | Demonstrates | +|---|---| +| `helper.sol` | `IHelperProgram` primitives (ATA/PDA create, SPL transfer, gas↔lamports) | +| `cached.sol` | The cached-precompile track end-to-end | +| `bench_cached.sol` / `bench_stacked.sol` | CU benchmarking of cached vs stacked call paths | +| `mixed.sol` | Mixing EVM and non-EVM calls under the dispatch rule | +| `cu.sol` (+ `cu_yul.yul`) | CU-measurement harness | +| `pdas_batch.sol` | Batch PDA derivation (`PdasBatch`) | +| `bridge.sol` | A bridge-interaction example | +| `orra.sol` | Sub-user-key / trade-key derivation pattern | + +--- + +## Import paths + +```solidity +// Precompile interfaces + bound singletons +import {ISystemProgram, ICrossProgramInvocation, IHelperProgram, IWithdraw, + CpiProgram, HelperProgram, SplCached, AssociatedSplCached} + from "@rome-protocol/rome-solidity/contracts/interface.sol"; + +// SPL ↔ ERC-20 +import {SPL_ERC20_cached} from "@rome-protocol/rome-solidity/contracts/erc20spl/erc20spl_cached.sol"; +import {ERC20SPLFactory} from "@rome-protocol/rome-solidity/contracts/erc20spl/erc20spl_factory.sol"; + +// Account model +import {RomeEVMAccount} from "@rome-protocol/rome-solidity/contracts/rome_evm_account.sol"; + +// Oracle (Chainlink-compatible) +import {IAggregatorV3Interface} from "@rome-protocol/rome-solidity/contracts/oracle/IAggregatorV3Interface.sol"; +``` + +npm publish is pending — consume via a github-pinned git dependency or by copying files (the CPI-precompile ABIs are also mirrored in `@rome-protocol/sdk`). diff --git a/scripts/deploy_meteora_factory.ts b/scripts/deploy_meteora_factory.ts deleted file mode 100644 index 9b402c8..0000000 --- a/scripts/deploy_meteora_factory.ts +++ /dev/null @@ -1,123 +0,0 @@ -import hardhat from "hardhat"; -import { getAddress, isAddress, isHex } from "viem"; -import { resolveERC20SPLFactoryAddress, saveFactoryDeployment } from "./lib/deployments.js"; - -const DEFAULT_PROG_DYNAMIC_AMM = - "0xccf802d4cccc84d7fb21b5f73b49d81a16c5b4c88ee32394e1c91d3588cc4080"; -const DEFAULT_PROG_DYNAMIC_VAULT = - "0x0fbfe8846d685cbdc62cca7e04c7e8f68dcc313ab31277e2e0112a2ec0e052e5"; -const DEFAULT_CPI_CONTRACT_ADDRESS = "0xFF00000000000000000000000000000000000008"; - -const VAULT_OVERRIDE_NETWORK = { - Mainnet: 0, - Devnet: 1, -} as const; - -type VaultOverrideNetwork = (typeof VAULT_OVERRIDE_NETWORK)[keyof typeof VAULT_OVERRIDE_NETWORK]; - -function resolveBytes32(value: string, name: string): `0x${string}` { - if (!isHex(value, { strict: true }) || value.length !== 66) { - throw new Error(`Invalid ${name}: expected bytes32 hex value, got ${value}`); - } - - return value as `0x${string}`; -} - -function resolveAddress(value: string, name: string): `0x${string}` { - if (!isAddress(value)) { - throw new Error(`Invalid ${name}: ${value}`); - } - - return getAddress(value); -} - -function resolveVaultOverrideNetwork(networkName: string): VaultOverrideNetwork { - const configured = process.env.VAULT_OVERRIDE_NETWORK?.trim().toLowerCase(); - if (configured) { - if (configured === "mainnet") { - return VAULT_OVERRIDE_NETWORK.Mainnet; - } - if (configured === "devnet") { - return VAULT_OVERRIDE_NETWORK.Devnet; - } - - throw new Error( - `Invalid VAULT_OVERRIDE_NETWORK: ${process.env.VAULT_OVERRIDE_NETWORK}. Use mainnet or devnet.`, - ); - } - - return ["local", ""].includes(networkName) - ? VAULT_OVERRIDE_NETWORK.Devnet - : VAULT_OVERRIDE_NETWORK.Mainnet; -} - -async function main() { - const { viem, networkName } = await hardhat.network.connect() as unknown as { - viem: { - getWalletClients: () => Promise>; - getPublicClient: () => Promise<{ - getBalance: (args: { address: `0x${string}` }) => Promise; - }>; - deployContract: ( - name: "MeteoraDAMMv1Factory", - args: readonly [`0x${string}`, `0x${string}`, `0x${string}`, `0x${string}`, VaultOverrideNetwork], - ) => Promise<{ address: `0x${string}` }>; - }; - networkName: string; - }; - - const [deployer] = await viem.getWalletClients(); - if (!deployer?.account) { - throw new Error("No deployer wallet found. Configure a funded account for this network."); - } - - const publicClient = await viem.getPublicClient(); - const progDynamicVault = resolveBytes32( - process.env.PROG_DYNAMIC_VAULT ?? DEFAULT_PROG_DYNAMIC_VAULT, - "PROG_DYNAMIC_VAULT", - ); - const progDynamicAmm = resolveBytes32( - process.env.PROG_DYNAMIC_AMM ?? DEFAULT_PROG_DYNAMIC_AMM, - "PROG_DYNAMIC_AMM", - ); - const cpiContractAddress = resolveAddress( - process.env.CPI_CONTRACT_ADDRESS ?? DEFAULT_CPI_CONTRACT_ADDRESS, - "CPI_CONTRACT_ADDRESS", - ); - const tokenFactoryAddress = resolveERC20SPLFactoryAddress(networkName); - const vaultOverrideNetwork = resolveVaultOverrideNetwork(networkName); - - console.log("Using network:", networkName); - console.log("Using deployer:", deployer.account.address); - console.log("prog_dynamic_vault:", progDynamicVault); - console.log("prog_dynamic_amm:", progDynamicAmm); - console.log("CPI contract address:", cpiContractAddress); - console.log("ERC20SPLFactory:", tokenFactoryAddress); - console.log( - "vault_override_network:", - vaultOverrideNetwork === VAULT_OVERRIDE_NETWORK.Devnet ? "Devnet" : "Mainnet", - ); - console.log( - "Account balance:", - (await publicClient.getBalance({ address: deployer.account.address })).toString(), - ); - - console.log("Deploying MeteoraDAMMv1Factory..."); - const factory = await viem.deployContract("MeteoraDAMMv1Factory", [ - tokenFactoryAddress, - progDynamicVault, - progDynamicAmm, - cpiContractAddress, - vaultOverrideNetwork, - ]); - - console.log("MeteoraDAMMv1Factory deployed to:", factory.address); - - saveFactoryDeployment(networkName, factory.address); - console.log(`Saved deployment to deployments/${networkName}.json`); -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/scripts/deploy_meteora_router.ts b/scripts/deploy_meteora_router.ts deleted file mode 100644 index d2341c0..0000000 --- a/scripts/deploy_meteora_router.ts +++ /dev/null @@ -1,66 +0,0 @@ -import hardhat from "hardhat"; -import { getAddress, isAddress } from "viem"; -import { resolveFactoryAddress, saveRouterDeployment } from "./lib/deployments.js"; - -const DEFAULT_CPI_CONTRACT_ADDRESS = "0xFF00000000000000000000000000000000000008"; - -function resolveAddress(value: string, name: string): `0x${string}` { - if (!isAddress(value)) { - throw new Error(`Invalid ${name}: ${value}`); - } - - return getAddress(value); -} - -async function main() { - const { viem, networkName } = await hardhat.network.connect() as unknown as { - viem: { - getWalletClients: () => Promise>; - getPublicClient: () => Promise<{ - getBalance: (args: { address: `0x${string}` }) => Promise; - }>; - deployContract: ( - name: "MeteoraDAMMv1Router", - args: readonly [`0x${string}`, `0x${string}`], - ) => Promise<{ address: `0x${string}` }>; - }; - networkName: string; - }; - - const [deployer] = await viem.getWalletClients(); - if (!deployer?.account) { - throw new Error("No deployer wallet found. Configure a funded account for this network."); - } - - const publicClient = await viem.getPublicClient(); - const factoryAddress = resolveFactoryAddress(networkName); - const cpiContractAddress = resolveAddress( - process.env.CPI_CONTRACT_ADDRESS ?? DEFAULT_CPI_CONTRACT_ADDRESS, - "CPI_CONTRACT_ADDRESS", - ); - - console.log("Using network:", networkName); - console.log("Using deployer:", deployer.account.address); - console.log("Using MeteoraDAMMv1Factory:", factoryAddress); - console.log("CPI contract address:", cpiContractAddress); - console.log( - "Account balance:", - (await publicClient.getBalance({ address: deployer.account.address })).toString(), - ); - - console.log("Deploying MeteoraDAMMv1Router..."); - const router = await viem.deployContract("MeteoraDAMMv1Router", [ - factoryAddress, - cpiContractAddress, - ]); - - console.log("MeteoraDAMMv1Router deployed to:", router.address); - - saveRouterDeployment(networkName, router.address); - console.log(`Saved deployment to deployments/${networkName}.json`); -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/scripts/register_meteora_pool.ts b/scripts/register_meteora_pool.ts deleted file mode 100644 index cd36816..0000000 --- a/scripts/register_meteora_pool.ts +++ /dev/null @@ -1,207 +0,0 @@ -import hardhat from "hardhat"; -import { getAddress, isHex, zeroAddress } from "viem"; -import { - readPoolDeployment, - resolveERC20SPLFactoryAddress, - resolveFactoryAddress, - savePoolDeployment, -} from "./lib/deployments.js"; -import { requireEnv } from "./lib/helpers.js"; -import { base58ToBytes32Hex } from "./lib/helpers.js"; - -function readBytes32FromHex(data: `0x${string}`, byteOffset: number): `0x${string}` { - const start = 2 + byteOffset * 2; - const end = start + 64; - - if (data.length < end) { - throw new Error(`Pool account data is too short to read bytes32 at offset ${byteOffset}.`); - } - - return `0x${data.slice(start, end)}` as `0x${string}`; -} - -function parsePoolTokenMints(data: `0x${string}`): { - tokenAMint: `0x${string}`; - tokenBMint: `0x${string}`; -} { - return { - tokenAMint: readBytes32FromHex(data, 8 + 32), - tokenBMint: readBytes32FromHex(data, 8 + 64), - }; -} - -async function waitForSuccess( - publicClient: { - waitForTransactionReceipt: ( - args: { hash: `0x${string}` }, - ) => Promise<{ status: string; blockNumber: bigint }>; - }, - txHash: `0x${string}`, - label: string, -): Promise<{ status: string; blockNumber: bigint }> { - const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - if (receipt.status !== "success") { - throw new Error(`${label} transaction failed: ${txHash}`); - } - - return receipt; -} - -async function ensureSplTokenRegistered(args: { - tokenFactory: any; - publicClient: any; - deployer: { account: { address: `0x${string}` } }; - mint: `0x${string}`; - label: string; -}): Promise<`0x${string}`> { - const { tokenFactory, publicClient, deployer, mint, label } = args; - - const existing = await tokenFactory.read.token_by_mint([mint]); - if (existing !== zeroAddress) { - const tokenAddress = getAddress(existing); - console.log(`${label} already registered in ERC20SPLFactory:`, tokenAddress); - return tokenAddress; - } - - console.log(`Registering ${label} in ERC20SPLFactory using MPL metadata...`); - const simulation = await tokenFactory.simulate.add_spl_token_with_metadata([mint], { - account: deployer.account, - }); - const txHash = await tokenFactory.write.add_spl_token_with_metadata(simulation.request); - console.log(`${label} registration tx:`, txHash); - await waitForSuccess(publicClient, txHash, `add_spl_token_with_metadata ${label}`); - - const tokenAddress = getAddress(await tokenFactory.read.token_by_mint([mint])); - if (tokenAddress === zeroAddress) { - throw new Error(`${label} registration completed but token_by_mint is still zero.`); - } - - console.log(`${label} wrapper address:`, tokenAddress); - return tokenAddress; -} - -async function main() { - const poolPubkey = base58ToBytes32Hex(requireEnv("POOL_ADDRESS"), "POOL_ADDRESS"); - - const { viem, networkName } = await hardhat.network.connect() as unknown as { - viem: { - getWalletClients: () => Promise>; - getPublicClient: () => Promise<{ - getBalance: (args: { address: `0x${string}` }) => Promise; - waitForTransactionReceipt: ( - args: { hash: `0x${string}` }, - ) => Promise<{ status: string; blockNumber: bigint }>; - }>; - getContractAt: (name: string, address: `0x${string}`, config?: unknown) => Promise; - }; - networkName: string; - }; - const factoryAddress = resolveFactoryAddress(networkName); - const tokenFactoryAddress = resolveERC20SPLFactoryAddress(networkName); - - const [deployer] = await viem.getWalletClients(); - if (!deployer?.account) { - throw new Error("No deployer wallet found. Configure a funded account for this network."); - } - const deployerAccount = deployer.account; - - const publicClient = await viem.getPublicClient(); - const factory = await viem.getContractAt("MeteoraDAMMv1Factory", factoryAddress); - const tokenFactory = await viem.getContractAt("ERC20SPLFactory", tokenFactoryAddress); - const cpiProgramAddress = await factory.read.cpi_program(); - const cpiProgram = await viem.getContractAt("ICrossProgramInvocation", cpiProgramAddress); - - console.log("Using network:", networkName); - console.log("Using deployer:", deployerAccount.address); - console.log("Using MeteoraDAMMv1Factory:", factoryAddress); - console.log("Using ERC20SPLFactory:", tokenFactoryAddress); - console.log("Pool pubkey:", poolPubkey); - console.log( - "Account balance:", - (await publicClient.getBalance({ address: deployerAccount.address })).toString(), - ); - - const poolInfo = await cpiProgram.read.account_info([poolPubkey]); - const lamports = poolInfo[0] as bigint; - const poolData = poolInfo[5] as `0x${string}`; - if (lamports === 0n) { - throw new Error(`Pool account does not exist on Solana: ${poolPubkey}`); - } - - const { tokenAMint, tokenBMint } = parsePoolTokenMints(poolData); - console.log("Pool token A mint:", tokenAMint); - console.log("Pool token B mint:", tokenBMint); - - const tokenAAddress = await ensureSplTokenRegistered({ - tokenFactory, - publicClient, - deployer: { account: deployerAccount }, - mint: tokenAMint, - label: "token A", - }); - const tokenBAddress = await ensureSplTokenRegistered({ - tokenFactory, - publicClient, - deployer: { account: deployerAccount }, - mint: tokenBMint, - label: "token B", - }); - - const existingWrappedPool = getAddress(await factory.read.getPool([tokenAAddress, tokenBAddress])); - if (existingWrappedPool !== zeroAddress) { - const existingDeployment = readPoolDeployment(networkName, poolPubkey); - if (!existingDeployment) { - throw new Error( - `Pool is already registered onchain at ${existingWrappedPool}, but deployments/${networkName}.json has no matching record.`, - ); - } - - console.log("Pool already registered in MeteoraDAMMv1Factory:", existingWrappedPool); - savePoolDeployment({ - networkName, - pubkey: poolPubkey, - address: existingWrappedPool, - txHash: existingDeployment.txHash as `0x${string}`, - blockNumber: BigInt(existingDeployment.blockNumber), - tokenAMint, - tokenBMint, - tokenAAddress, - tokenBAddress, - }); - console.log(`Updated deployments/${networkName}.json with token details.`); - return; - } - - console.log("Registering pool in MeteoraDAMMv1Factory..."); - const txHash = await factory.write.addPool([poolPubkey], { - account: deployerAccount, - }); - console.log("Pool registration tx:", txHash); - - const receipt = await waitForSuccess(publicClient, txHash, "addPool"); - const wrappedPoolAddress = getAddress(await factory.read.getPool([tokenAAddress, tokenBAddress])); - if (wrappedPoolAddress === zeroAddress) { - throw new Error("Pool registration succeeded but getPool returned zero address."); - } - - console.log("Wrapped pool address:", wrappedPoolAddress); - - savePoolDeployment({ - networkName, - pubkey: poolPubkey, - address: wrappedPoolAddress, - txHash, - blockNumber: receipt.blockNumber, - tokenAMint, - tokenBMint, - tokenAAddress, - tokenBAddress, - }); - - console.log(`Saved pool deployment to deployments/${networkName}.json`); -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/scripts/setup-local.ts b/scripts/setup-local.ts index f747f3a..6541b4b 100644 --- a/scripts/setup-local.ts +++ b/scripts/setup-local.ts @@ -18,28 +18,15 @@ import { SPL_MINTS_DEVNET } from "./bridge/constants.js"; * → ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 * * What this does: - * 1. Deploys MeteoraDAMMv1Factory + registers the pre-seeded SOL/USDC pool - * 2. Deploys Oracle Gateway V2 (PythPullAdapter, SwitchboardV3Adapter, Factory, BatchReader) - * 3. Creates Pyth Pull feed adapters for pre-seeded accounts (BTC, USDC, USDT, WETH) + * 1. Deploys Oracle Gateway V2 (PythPullAdapter, SwitchboardV3Adapter, Factory, BatchReader) + * 2. Creates Pyth Pull + Switchboard feed adapters for pre-seeded accounts + * 3. Deploys the SPL-ERC20 wrappers + RomeBridgeWithdraw * 4. Saves everything to deployments/local.json * * Usage: * npx hardhat run scripts/setup-local.ts --network local */ -// Pre-seeded Meteora pool from the Rome EVM program (mainnet snapshot) -// Base58: 5yuefgbJJpmFNK2iiYbLSpv1aZXq7F9AUKkZKErTYCvs -const POOL_PUBKEY: `0x${string}` = - "0x4a02cdcd4da84ccd595ceff987b1738f19ee8d39afd64c91c6c123c47db61b18"; - -// Meteora program IDs (base58-decoded) -const PROG_DYNAMIC_AMM: `0x${string}` = - "0xccf802d4cccc84d7fb21b5f73b49d81a16c5b4c88ee32394e1c91d3588cc4080"; // Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB -const PROG_DYNAMIC_VAULT: `0x${string}` = - "0x0fbfe8846d685cbdc62cca7e04c7e8f68dcc313ab31277e2e0112a2ec0e052e5"; // 24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi -const CPI_ADDRESS: `0x${string}` = "0xFF00000000000000000000000000000000000008"; -const VAULT_OVERRIDE_NETWORK_MAINNET = 0; - // Oracle program IDs const PYTH_RECEIVER_PROGRAM_ID: `0x${string}` = "0x0cb7fabb52f7a648bb5b317d9a018b9057cb024774fafe01e6c4df98cc385881"; // rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ @@ -110,50 +97,10 @@ async function main() { console.log("Balance:", balance.toString()); console.log(); - const erc20SplFactoryAddress = resolveAddress( - process.env.ERC20_SPL_FACTORY_ADDRESS ?? "", - "ERC20_SPL_FACTORY_ADDRESS", - ); - const deployments: Record = {}; - // ─── 1. Meteora DAMMv1 Factory ─── - console.log("=== 1/5 Deploying MeteoraDAMMv1Factory ==="); - const factory = await viem.deployContract("MeteoraDAMMv1Factory", [ - erc20SplFactoryAddress, - PROG_DYNAMIC_VAULT, - PROG_DYNAMIC_AMM, - CPI_ADDRESS, - VAULT_OVERRIDE_NETWORK_MAINNET, - ]); - console.log(" Factory:", factory.address); - deployments.MeteoraDAMMv1Factory = { address: factory.address }; - - // ─── 2. Register pre-seeded pool ─── - console.log("\n=== 2/5 Registering pre-seeded Meteora pool ==="); - console.log(" Pool pubkey:", POOL_PUBKEY); - - const addPoolTx = await factory.write.addPool([POOL_PUBKEY], { - account: deployer.account, - }); - const addPoolReceipt = await publicClient.waitForTransactionReceipt({ hash: addPoolTx }); - console.log(" Status:", addPoolReceipt.status); - - const poolCount = await factory.read.allPoolsLength(); - const poolAddress = await factory.read.allPools([poolCount - 1n]); - console.log(" Pool EVM address:", poolAddress); - - deployments.MeteoraDAMMv1Pools = [ - { - pubkey: POOL_PUBKEY, - address: poolAddress, - txHash: addPoolTx, - blockNumber: addPoolReceipt.blockNumber.toString(), - }, - ]; - - // ─── 3. Oracle Gateway V2 ─── - console.log("\n=== 3/5 Deploying Oracle Gateway V2 ==="); + // ─── 1. Oracle Gateway V2 ─── + console.log("=== 1/4 Deploying Oracle Gateway V2 ==="); const pythImpl = await viem.deployContract("PythPullAdapter", []); console.log(" PythPullAdapter impl:", pythImpl.address); @@ -186,8 +133,8 @@ async function main() { deployedAt: new Date().toISOString(), }; - // ─── 4. Create Pyth feeds ─── - console.log("\n=== 4/5 Creating Pyth Pull feed adapters ==="); + // ─── 2. Create Pyth feeds ─── + console.log("\n=== 2/4 Creating Pyth Pull feed adapters ==="); const feeds: any[] = []; for (const feed of PYTH_FEEDS) { @@ -221,8 +168,8 @@ async function main() { deployments.OracleGatewayV2.feeds = feeds; - // ─── 5. Create Switchboard feeds ─── - console.log("\n=== 5/5 Creating Switchboard feed adapters ==="); + // ─── 3. Create Switchboard feeds ─── + console.log("\n=== 3/4 Creating Switchboard feed adapters ==="); const sbFeeds: any[] = []; for (const feed of SWITCHBOARD_FEEDS) { @@ -256,8 +203,8 @@ async function main() { deployments.OracleGatewayV2.switchboardFeeds = sbFeeds; - // ─── 6. Bridge contracts ─── - console.log("\n=== 6/6 Deploying Rome Bridge contracts ==="); + // ─── 4. Bridge contracts ─── + console.log("\n=== 4/4 Deploying Rome Bridge contracts ==="); // CPI precompile address — defined in contracts/interface.sol as 0xff..08. const cpiProgramAddress = "0xFF00000000000000000000000000000000000008" as `0x${string}`; @@ -314,7 +261,6 @@ async function main() { console.log("\n=== Setup Complete ==="); console.log(`Deployments saved to: ${filePath}`); - console.log(`Meteora: factory + 1 pool`); console.log(`Pyth feeds: ${successFeeds.length}/${feeds.length} created`); console.log(`Switchboard feeds: ${successSbFeeds.length}/${sbFeeds.length} created`); console.log(`Bridge: WUSDC wrapper + WETH wrapper + RomeBridgeWithdraw`); @@ -323,7 +269,6 @@ async function main() { console.log(`Failed feeds: ${allFailed.map((f: any) => f.pair).join(", ")}`); } console.log("\nRun tests:"); - console.log(" npx hardhat test tests/damm_v1_pool.integration.ts --network local"); console.log(" npx hardhat test tests/oracle/ # parser unit tests (no network needed)"); } diff --git a/tests/cpi/PdasBatch.test.ts b/tests/cpi/PdasBatch.test.ts index 7dc0640..548c475 100644 --- a/tests/cpi/PdasBatch.test.ts +++ b/tests/cpi/PdasBatch.test.ts @@ -7,10 +7,9 @@ import hardhat from "hardhat"; * * The on-chain `derive` paths require the CPI precompile (`0xFF…08`), * which doesn't exist on hardhatMainnet. Network-independent shape - * assertions live here; live-precompile assertions are exercised via - * the Meteora damm_v1_pool integration tests (same library, real - * call path) — see `tests/damm_v1_pool.integration.ts --network local` - * or against a live Rome devnet/testnet chain. + * assertions live here; live-precompile assertions are exercised + * against a live Rome devnet/testnet chain (the CPI precompile is + * required for the real derive path). * * Empty-group path IS exercised here because the precompile fallback * for N=0 returns an empty array without any syscall — pure ABI diff --git a/tests/damm_v1_pool.integration.ts b/tests/damm_v1_pool.integration.ts deleted file mode 100644 index e064c25..0000000 --- a/tests/damm_v1_pool.integration.ts +++ /dev/null @@ -1,413 +0,0 @@ -import { before, describe, it } from "node:test"; -import assert from "node:assert/strict"; -import hardhat from "hardhat"; -import { readDeployments, PoolDeployment, DeploymentsFile } from "../scripts/lib/deployments.js"; -import { requireEnv } from "../scripts/lib/helpers.js"; - - -function isHex32(value: string): boolean { - return /^0x[0-9a-fA-F]{64}$/.test(value); -} - -function isHex20(value: string): boolean { - return /^0x[0-9a-fA-F]{40}$/.test(value); -} - -function isZeroBytes32(value: string): boolean { - return /^0x0{64}$/i.test(value); -} - -function pickPoolDeployment(deployments: DeploymentsFile): { - pubkey: string; - address: `0x${string}`; -} { - const pools = deployments.MeteoraDAMMv1Pools ?? []; - if (pools.length === 0) { - throw new Error( - "No MeteoraDAMMv1Pools found in deployments file. Deploy a pool first.", - ); - } - - const wantedPubkey = process.env.POOL_PUBKEY?.toLowerCase(); - if (wantedPubkey) { - const found = pools.find((p: PoolDeployment) => p.pubkey.toLowerCase() === wantedPubkey); - if (!found) { - throw new Error( - `POOL_PUBKEY=${process.env.POOL_PUBKEY} not found in deployments file.`, - ); - } - - return { - pubkey: found.pubkey, - address: found.address as `0x${string}`, - }; - } - - const latest = pools[pools.length - 1]; - return { - pubkey: latest.pubkey, - address: latest.address as `0x${string}`, - }; -} - -describe("DAMMv1Pool integration", function () { - let poolAddress: `0x${string}`; - let poolPubkey: string; - let selectedPool: PoolDeployment; - let pool: any; - let erc20pool: any; - let publicClient: Awaited>["viem"] extends infer V - ? V extends { getPublicClient: () => Promise } - ? P - : never - : never; - - before(async function () { - const { viem, networkName } = await hardhat.network.connect(); - publicClient = await viem.getPublicClient(); - - const deployments = readDeployments(networkName); - const selected = pickPoolDeployment(deployments); - selectedPool = deployments.MeteoraDAMMv1Pools?.find( - (poolDeployment) => - poolDeployment.pubkey.toLowerCase() === selected.pubkey.toLowerCase() - || poolDeployment.address.toLowerCase() === selected.address.toLowerCase(), - ) ?? { - pubkey: selected.pubkey, - address: selected.address, - txHash: "", - blockNumber: "0", - tokenAMint: "", - tokenBMint: "", - tokenAAddress: "", - tokenBAddress: "", - }; - - poolAddress = selected.address; - poolPubkey = selected.pubkey; - - erc20pool = await viem.getContractAt("ERC20DAMMv1Pool", poolAddress); - pool = await viem.getContractAt("DAMMv1Pool", await erc20pool.read.internal_pool()); - - const code = await publicClient.getCode({ address: poolAddress }); - assert.ok(code && code !== "0x", `No contract code at ${poolAddress}`); - - console.log("Testing ERC20DAMMv1Pool at:", poolAddress); - console.log("Expected pool pubkey:", poolPubkey); - }); - - it("has correct immutable-like core addresses/state initialized", async function () { - const onchainPoolAddress = await pool.read.pool_address(); - const progDynamicVault = await pool.read.prog_dynamic_vault(); - const progDynamicAmm = await pool.read.prog_dynamic_amm(); - - assert.equal( - onchainPoolAddress.toLowerCase(), - poolPubkey.toLowerCase(), - "pool_address does not match deployment record pubkey", - ); - - assert.ok(isHex32(onchainPoolAddress), "pool_address must be bytes32"); - assert.ok(isHex32(progDynamicVault), "prog_dynamic_vault must be bytes32"); - assert.ok(isHex32(progDynamicAmm), "prog_dynamic_amm must be bytes32"); - - assert.ok(!isZeroBytes32(progDynamicVault), "prog_dynamic_vault must not be zero"); - assert.ok(!isZeroBytes32(progDynamicAmm), "prog_dynamic_amm must not be zero"); - }); - - it("has non-empty parsed pool state fields", async function () { - const lpMint = await pool.read.lp_mint(); - const tokenAMint = await pool.read.token_a_mint(); - const tokenBMint = await pool.read.token_b_mint(); - const aVault = await pool.read.a_vault(); - const bVault = await pool.read.b_vault(); - const aVaultLp = await pool.read.a_vault_lp(); - const bVaultLp = await pool.read.b_vault_lp(); - - assert.ok(isHex32(lpMint), "lp_mint must be bytes32"); - assert.ok(isHex32(tokenAMint), "token_a_mint must be bytes32"); - assert.ok(isHex32(tokenBMint), "token_b_mint must be bytes32"); - assert.ok(isHex32(aVault), "a_vault must be bytes32"); - assert.ok(isHex32(bVault), "b_vault must be bytes32"); - assert.ok(isHex32(aVaultLp), "a_vault_lp must be bytes32"); - assert.ok(isHex32(bVaultLp), "b_vault_lp must be bytes32"); - - assert.ok(!isZeroBytes32(lpMint), "lp_mint must not be zero"); - assert.ok(!isZeroBytes32(tokenAMint), "token_a_mint must not be zero"); - assert.ok(!isZeroBytes32(tokenBMint), "token_b_mint must not be zero"); - assert.ok(!isZeroBytes32(aVault), "a_vault must not be zero"); - assert.ok(!isZeroBytes32(bVault), "b_vault must not be zero"); - assert.ok(!isZeroBytes32(aVaultLp), "a_vault_lp must not be zero"); - assert.ok(!isZeroBytes32(bVaultLp), "b_vault_lp must not be zero"); - - assert.notEqual( - tokenAMint.toLowerCase(), - tokenBMint.toLowerCase(), - "token_a_mint and token_b_mint must differ", - ); - assert.notEqual( - aVault.toLowerCase(), - bVault.toLowerCase(), - "a_vault and b_vault must differ", - ); - }); - - it("returns vault structs with sane values", async function () { - const vaultA = await pool.read.vault_a(); - const vaultB = await pool.read.vault_b(); - - const vaultAEnabled = vaultA[0]; - const vaultABumps = vaultA[1]; - const vaultATotalAmount = vaultA[2]; - const vaultATokenVault = vaultA[3]; - const vaultAFeeVault = vaultA[4]; - const vaultATokenMint = vaultA[5]; - const vaultALpMint = vaultA[6]; - const vaultALockedProfitTracker = vaultA[7]; - - const vaultBEnabled = vaultB[0]; - const vaultBBumps = vaultB[1]; - const vaultBTotalAmount = vaultB[2]; - const vaultBTokenVault = vaultB[3]; - const vaultBFeeVault = vaultB[4]; - const vaultBTokenMint = vaultB[5]; - const vaultBLpMint = vaultB[6]; - const vaultBLockedProfitTracker = vaultB[7]; - - assert.ok(typeof vaultAEnabled === "number", "vault_a.enabled must be number"); - assert.ok(typeof vaultBEnabled === "number", "vault_b.enabled must be number"); - - assert.ok(typeof vaultABumps.vault_bump === "number", "vault_a.bumps.vault_bump must be number"); - assert.ok(typeof vaultABumps.token_vault_bump === "number", "vault_a.bumps.token_vault_bump must be number"); - assert.ok(typeof vaultBBumps.vault_bump === "number", "vault_b.bumps.vault_bump must be number"); - assert.ok(typeof vaultBBumps.token_vault_bump === "number", "vault_b.bumps.token_vault_bump must be number"); - - assert.ok(typeof vaultATotalAmount === "bigint", "vault_a.total_amount must be bigint"); - assert.ok(typeof vaultBTotalAmount === "bigint", "vault_b.total_amount must be bigint"); - - assert.ok(isHex32(vaultATokenVault), "vault_a.token_vault must be bytes32"); - assert.ok(isHex32(vaultAFeeVault), "vault_a.fee_vault must be bytes32"); - assert.ok(isHex32(vaultATokenMint), "vault_a.token_mint must be bytes32"); - assert.ok(isHex32(vaultALpMint), "vault_a.lp_mint must be bytes32"); - - assert.ok(isHex32(vaultBTokenVault), "vault_b.token_vault must be bytes32"); - assert.ok(isHex32(vaultBFeeVault), "vault_b.fee_vault must be bytes32"); - assert.ok(isHex32(vaultBTokenMint), "vault_b.token_mint must be bytes32"); - assert.ok(isHex32(vaultBLpMint), "vault_b.lp_mint must be bytes32"); - - assert.ok(vaultATotalAmount >= 0n, "vault_a.total_amount must be non-negative"); - assert.ok(vaultBTotalAmount >= 0n, "vault_b.total_amount must be non-negative"); - - assert.ok( - typeof vaultALockedProfitTracker.last_updated_locked_profit === "bigint", - "vault_a.locked_profit_tracker.last_updated_locked_profit must be bigint", - ); - assert.ok( - typeof vaultALockedProfitTracker.last_report === "bigint", - "vault_a.locked_profit_tracker.last_report must be bigint", - ); - assert.ok( - typeof vaultALockedProfitTracker.locked_profit_degradation === "bigint", - "vault_a.locked_profit_tracker.locked_profit_degradation must be bigint", - ); - - assert.ok( - typeof vaultBLockedProfitTracker.last_updated_locked_profit === "bigint", - "vault_b.locked_profit_tracker.last_updated_locked_profit must be bigint", - ); - assert.ok( - typeof vaultBLockedProfitTracker.last_report === "bigint", - "vault_b.locked_profit_tracker.last_report must be bigint", - ); - assert.ok( - typeof vaultBLockedProfitTracker.locked_profit_degradation === "bigint", - "vault_b.locked_profit_tracker.locked_profit_degradation must be bigint", - ); - }); - - it("returns reserves", async function () { - const reserves = await pool.read.get_reserves(); - - assert.ok( - typeof reserves.a_reserve === "bigint", - "a_reserve must be bigint", - ); - assert.ok( - typeof reserves.b_reserve === "bigint", - "b_reserve must be bigint", - ); - - assert.ok(reserves.a_reserve >= 0n, "a_reserve must be >= 0"); - assert.ok(reserves.b_reserve >= 0n, "b_reserve must be >= 0"); - }); - - it("returns price for TokenA and TokenB when reserves are non-zero", async function () { - const reserves = await pool.read.get_reserves(); - - if (reserves.a_reserve === 0n || reserves.b_reserve === 0n) { - this.skip(); - return; - } - - const priceA = await pool.read.get_price_e18([0]); // PoolToken.TokenA - const priceB = await pool.read.get_price_e18([1]); // PoolToken.TokenB - - assert.ok(priceA > 0n, "priceA must be > 0"); - assert.ok(priceB > 0n, "priceB must be > 0"); - }); - - it("returns fees in e18 when denominators are non-zero", async function () { - const fees = await pool.read.fees(); - const tradeFeeDenominator = fees[1]; - const protocolTradeFeeDenominator = fees[3]; - - assert.ok( - tradeFeeDenominator > 0n, - "trade_fee_denominator must be > 0 for get_fees_e18 test", - ); - assert.ok( - protocolTradeFeeDenominator > 0n, - "protocol_trade_fee_denominator must be > 0 for get_fees_e18 test", - ); - - const feeE18 = await pool.read.get_fees_e18(); - assert.ok(feeE18 >= 0n, "feeE18 must be >= 0"); - }); - - it("protocol fee accounts are valid bytes32 values", async function () { - const protocolTokenAFee = await pool.read.protocol_token_a_fee(); - const protocolTokenBFee = await pool.read.protocol_token_b_fee(); - - assert.ok(isHex32(protocolTokenAFee), "protocol_token_a_fee must be bytes32"); - assert.ok(isHex32(protocolTokenBFee), "protocol_token_b_fee must be bytes32"); - }); - - it("deployed contract address is a valid EVM address", async function () { - assert.ok(isHex20(poolAddress), `Pool address is not a valid EVM address: ${poolAddress}`); - }); - - it("can optionally refresh cached state via update_state()", async function () { - const { viem } = await hardhat.network.connect(); - const [deployer] = await viem.getWalletClients(); - if (!deployer?.account) { - throw new Error("No deployer wallet available for write test."); - } - - const txHash = await pool.write.update_state([], { - account: deployer.account, - }); - - const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - assert.equal(receipt.status, "success", "update_state tx failed"); - }); - - it("can invoke_swap via explicit accounts", async function () { - const tokenIn = 0; - const amountIn = BigInt(10000); - const minAmountOut = 0n; - - const { viem, networkName } = await hardhat.network.connect(); - const [deployer] = await viem.getWalletClients(); - if (!deployer?.account) { - throw new Error("No deployer wallet available for invoke_swap test."); - } - - const deployments = readDeployments(networkName); - const erc20SplFactoryAddress = deployments.ERC20SPLFactory?.address; - if (!erc20SplFactoryAddress) { - console.log("erc20SplFactory deployment not found"); - this.skip(); - return; - } - - const tokenInAddress = (tokenIn === 0 - ? selectedPool.tokenAAddress - : selectedPool.tokenBAddress) as `0x${string}`; - const tokenOutAddress = (tokenIn === 0 - ? selectedPool.tokenBAddress - : selectedPool.tokenAAddress) as `0x${string}`; - - const tokenInContract = await viem.getContractAt("SPL_ERC20", tokenInAddress, { - client: { - public: publicClient, - wallet: deployer, - }, - }); - const tokenOutContract = await viem.getContractAt("SPL_ERC20", tokenOutAddress, { - client: { - public: publicClient, - wallet: deployer, - }, - }); - const erc20SplFactory = await viem.getContractAt("ERC20SPLFactory", erc20SplFactoryAddress, { - client: { - public: publicClient, - wallet: deployer, - }, - }); - const usersAddress = await erc20SplFactory.read.users(); - const users = await viem.getContractAt("ERC20Users", usersAddress, { - client: { - public: publicClient, - wallet: deployer, - }, - }); - - // factory.create_user was removed (operator-subsidy cleanup). User - // registration in ERC20Users now happens implicitly via the first - // wrapper-mediated mutation. This test's downstream addLiquidity / - // swap flow goes through wrapper.transferFrom which auto-fires - // ensure_user(spender) — so the deployer ends up registered as a - // side effect of the test's own operations. - let payer: `0x${string}` | undefined; - try { - payer = await users.read.get_user([deployer.account.address]) as `0x${string}`; - } catch { - // Not yet registered — first wrapper mutation below will register. - } - console.log("Payer is ", payer); - - for (const [tokenContract, label] of [ - [tokenInContract, "token in"], - [tokenOutContract, "token out"], - ] as const) { - const ensureAccountTxHash = await tokenContract.write.ensure_token_account([deployer.account.address], { - account: deployer.account, - }); - const ensureAccountReceipt = await publicClient.waitForTransactionReceipt({ hash: ensureAccountTxHash }); - assert.equal(ensureAccountReceipt.status, "success", `ensure ${label} token account tx failed`); - } - - console.log("User input token account ", await tokenInContract.read.get_token_account([deployer.account.address])); - - const inputBalanceBefore = await tokenInContract.read.balanceOf([deployer.account.address]); - const outputBalanceBefore = await tokenOutContract.read.balanceOf([deployer.account.address]); - - assert.ok( - inputBalanceBefore >= amountIn, - `swap user must hold at least ${amountIn} input tokens before swap`, - ); - - const swapTxHash = await erc20pool.write.swapExactTokensForTokens( - [tokenInAddress, amountIn, minAmountOut], - { - account: deployer.account, - }, - ); - const swapReceipt = await publicClient.waitForTransactionReceipt({ hash: swapTxHash }); - assert.equal(swapReceipt.status, "success", "swapExactTokensForTokens tx failed"); - - const inputBalanceAfter = await tokenInContract.read.balanceOf([deployer.account.address]); - const outputBalanceAfter = await tokenOutContract.read.balanceOf([deployer.account.address]); - - assert.equal( - inputBalanceAfter, - inputBalanceBefore - amountIn, - "input token balance must decrease by the swap amount", - ); - assert.ok( - outputBalanceAfter > outputBalanceBefore, - "output token balance must increase after swap", - ); - }); -}); diff --git a/tests/damm_v1_router.integration.ts b/tests/damm_v1_router.integration.ts deleted file mode 100644 index 913c11f..0000000 --- a/tests/damm_v1_router.integration.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { before, describe, it } from "node:test"; -import assert from "node:assert/strict"; -import hardhat from "hardhat"; -import { getAddress, isAddress, zeroAddress } from "viem"; -import { readDeployments } from "../scripts/lib/deployments.js"; - -function resolveDeploymentAddress( - networkName: string, - key: "MeteoraDAMMv1Factory" | "MeteoraDAMMv1Router" | "ERC20SPLFactory", -): `0x${string}` { - const deployments = readDeployments(networkName); - const address = deployments[key]?.address; - - if (!address) { - throw new Error(`${key} is not deployed for ${networkName}. Run the deployment script first.`); - } - - if (!isAddress(address)) { - throw new Error(`Invalid ${key} address in deployments/${networkName}.json: ${address}`); - } - - return getAddress(address); -} - -function isHex32(value: string): boolean { - return /^0x[0-9a-fA-F]{64}$/.test(value); -} - -function isZeroBytes32(value: string): boolean { - return /^0x0{64}$/i.test(value); -} - -async function waitForSuccess( - publicClient: { - waitForTransactionReceipt: ( - args: { hash: `0x${string}` }, - ) => Promise<{ status: string; blockNumber: bigint }>; - }, - txHash: `0x${string}`, - label: string, -): Promise<{ status: string; blockNumber: bigint }> { - const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - assert.equal(receipt.status, "success", `${label} transaction failed`); - return receipt; -} - -async function ensureUser(_factoryFromUser: any, users: any, _publicClient: any, user: any): Promise { - // factory.create_user was removed (operator-subsidy cleanup). User - // registration in ERC20Users now happens implicitly via the first - // wrapper-mediated mutation. This helper is now a noop probe — if - // the user isn't yet registered, downstream test ops (transferFrom - // via the router) auto-fire ensure_user(spender) as a side effect. - try { - await users.read.get_user([user.account.address]); - } catch { - // not yet registered; will be registered on first wrapper mutation - } -} - -async function ensureTokenAccount(token: any, publicClient: any, owner: any, label: string): Promise { - const txHash = await token.write.ensure_token_account([owner.account.address], { - account: owner.account, - }); - await waitForSuccess(publicClient, txHash, label); -} - -async function assertSolanaAccountExists( - cpiProgram: any, - pubkey: `0x${string}` | string, - label: string, -): Promise { - const info = await cpiProgram.read.account_info([pubkey]); - assert.ok(info[0] > 0n, `${label} must exist on Solana`); -} - -async function assertSolanaProgramExists( - cpiProgram: any, - pubkey: `0x${string}` | string, - label: string, -): Promise { - const info = await cpiProgram.read.account_info([pubkey]); - assert.ok(info[0] > 0n, `${label} program account must exist on Solana`); - assert.equal(info[4], true, `${label} program account must be executable`); -} - -function readU64LE(data: `0x${string}` | string, offset: number): bigint { - const hex = data.startsWith("0x") ? data.slice(2) : data; - const start = offset * 2; - const end = start + 16; - const value = hex.slice(start, end); - - let result = 0n; - for (let i = 0; i < 8; i++) { - const byteHex = value.slice(i * 2, i * 2 + 2) || "00"; - result |= BigInt(parseInt(byteHex, 16)) << (8n * BigInt(i)); - } - - return result; -} - -async function readSplTokenAmount( - cpiProgram: any, - pubkey: `0x${string}` | string, -): Promise { - const info = await cpiProgram.read.account_info([pubkey]); - if (info[0] === 0n) { - return 0n; - } - - return readU64LE(info[5], 64); -} - -async function findExistingConfig(factory: any, cpiProgram: any, maxIndex = 16): Promise<`0x${string}`> { - for (let index = 0n; index < BigInt(maxIndex); index++) { - const config = await factory.read.deriveConfigKey([index]); - const info = await cpiProgram.read.account_info([config]); - if (info[0] > 0n) { - return config; - } - } - - throw new Error(`No existing DAMM config found in first ${maxIndex} config PDAs.`); -} - -async function createSplToken(args: { - factory: any; - publicClient: any; - viem: any; - owner: any; - name: string; - symbol: string; -}): Promise<{ - mintId: `0x${string}`; - tokenAddress: `0x${string}`; - token: any; -}> { - const { factory, publicClient, viem, owner, name, symbol } = args; - - const [mintId] = await factory.read.get_current_mint([owner.account.address]); - - const createTokenTxHash = await factory.write.create_token_mint([], { - account: owner.account, - }); - await waitForSuccess(publicClient, createTokenTxHash, `create_token_mint ${symbol}`); - - const initTokenTxHash = await factory.write.init_token_mint([mintId], { - account: owner.account, - }); - await waitForSuccess(publicClient, initTokenTxHash, `init_token_mint ${symbol}`); - - const addTokenSimulation = await factory.simulate.add_spl_token_no_metadata( - [mintId, name, symbol], - { - account: owner.account, - }, - ); - const tokenAddress = addTokenSimulation.result; - - const addTokenTxHash = await factory.write.add_spl_token_no_metadata(addTokenSimulation.request); - await waitForSuccess(publicClient, addTokenTxHash, `add_spl_token_no_metadata ${symbol}`); - - const token = await viem.getContractAt("SPL_ERC20", tokenAddress, { - client: { - public: publicClient, - wallet: owner, - }, - }); - - return { mintId, tokenAddress, token }; -} - -describe("MeteoraDAMMv1Router integration", { concurrency: false }, function () { - let viem: any; - let publicClient: any; - let deployer: any; - let factory: any; - let factoryFromUser: any; - let routerFromUser: any; - let erc20SplFactory: any; - let erc20FactoryFromUser: any; - let users: any; - let cpiProgram: any; - let networkName: string; - - let factoryAddress: `0x${string}`; - let routerAddress: `0x${string}`; - let erc20SplFactoryAddress: `0x${string}`; - - let tokenAMint: `0x${string}`; - let tokenBMint: `0x${string}`; - let tokenAAddress: `0x${string}`; - let tokenBAddress: `0x${string}`; - let tokenAFromUser: any; - let tokenBFromUser: any; - let payer: `0x${string}`; - let userTokenAccountA: `0x${string}`; - let userTokenAccountB: `0x${string}`; - let progDynamicVault: `0x${string}`; - let progDynamicAmm: `0x${string}`; - let previewVaultA: any; - let previewVaultB: any; - let poolConfig: `0x${string}`; - - let poolPubkey: `0x${string}`; - let wrappedPoolAddress: `0x${string}`; - let wrappedPool: any; - let internalPool: any; - let userPoolLp: `0x${string}`; - let addedLiquidityLpAmount = 0n; - - const mintAmount = 2_000_000_000_000n; - const poolTokenAAmount = 500_000_000_000n; - const poolTokenBAmount = 500_000_000_000n; - const swapAmountIn = 100_000_000n; - const minAmountOut = 0n; - const addLiquidityTokenAAmount = 100_000_000n; - const addLiquidityTokenBAmount = 100_000_000n; - - before(async function () { - const { viem: connectedViem, networkName: connectedNetworkName } = await hardhat.network.connect() as unknown as { - viem: { - getPublicClient: () => Promise; - getWalletClients: () => Promise; - getContractAt: (name: string, address: `0x${string}`, config?: unknown) => Promise; - }; - networkName: string; - }; - - viem = connectedViem; - networkName = connectedNetworkName; - publicClient = await viem.getPublicClient(); - - const walletClients = await viem.getWalletClients(); - deployer = walletClients[0]; - if (!deployer?.account) { - throw new Error("No wallet client available."); - } - - factoryAddress = resolveDeploymentAddress(networkName, "MeteoraDAMMv1Factory"); - routerAddress = resolveDeploymentAddress(networkName, "MeteoraDAMMv1Router"); - erc20SplFactoryAddress = resolveDeploymentAddress(networkName, "ERC20SPLFactory"); - - factory = await viem.getContractAt("MeteoraDAMMv1Factory", factoryAddress); - factoryFromUser = await viem.getContractAt("MeteoraDAMMv1Factory", factoryAddress, { - client: { - public: publicClient, - wallet: deployer, - }, - }); - erc20SplFactory = await viem.getContractAt("ERC20SPLFactory", erc20SplFactoryAddress); - erc20FactoryFromUser = await viem.getContractAt("ERC20SPLFactory", erc20SplFactoryAddress, { - client: { - public: publicClient, - wallet: deployer, - }, - }); - routerFromUser = await viem.getContractAt("MeteoraDAMMv1Router", routerAddress, { - client: { - public: publicClient, - wallet: deployer, - }, - }); - - const factoryCode = await publicClient.getCode({ address: factoryAddress }); - const routerCode = await publicClient.getCode({ address: routerAddress }); - const erc20FactoryCode = await publicClient.getCode({ address: erc20SplFactoryAddress }); - assert.ok(factoryCode && factoryCode !== "0x", `No contract code at ${factoryAddress}`); - assert.ok(routerCode && routerCode !== "0x", `No contract code at ${routerAddress}`); - assert.ok(erc20FactoryCode && erc20FactoryCode !== "0x", `No contract code at ${erc20SplFactoryAddress}`); - - const usersAddress = await erc20SplFactory.read.users(); - users = await viem.getContractAt("ERC20Users", usersAddress); - await ensureUser(erc20FactoryFromUser, users, publicClient, deployer); - payer = await users.read.get_user([deployer.account.address]); - - const configuredTokenFactoryAddress = getAddress(await factory.read.token_factory()); - assert.equal( - configuredTokenFactoryAddress.toLowerCase(), - erc20SplFactoryAddress.toLowerCase(), - "MeteoraDAMMv1Factory must be configured with the deployed ERC20SPLFactory", - ); - - assert.ok(isHex32(payer), "payer must be bytes32"); - assert.notEqual(payer, `0x${"0".repeat(64)}`, "payer must not be zero"); - - const cpiProgramAddress = await factory.read.cpi_program(); - cpiProgram = await viem.getContractAt("ICrossProgramInvocation", cpiProgramAddress); - progDynamicVault = await factory.read.prog_dynamic_vault(); - progDynamicAmm = await factory.read.prog_dynamic_amm(); - - assert.ok(isHex32(progDynamicVault), "prog_dynamic_vault must be bytes32"); - assert.ok(isHex32(progDynamicAmm), "prog_dynamic_amm must be bytes32"); - assert.ok(!isZeroBytes32(progDynamicVault), "prog_dynamic_vault must not be zero"); - assert.ok(!isZeroBytes32(progDynamicAmm), "prog_dynamic_amm must not be zero"); - - await assertSolanaAccountExists(cpiProgram, payer, "payer"); - await assertSolanaProgramExists(cpiProgram, progDynamicVault, "prog_dynamic_vault"); - await assertSolanaProgramExists(cpiProgram, progDynamicAmm, "prog_dynamic_amm"); - - poolConfig = "0x644ca100bdd0fb4a40a19bd736434cec22a01b0f380626464ad69a115df8ef80"; - await assertSolanaAccountExists(cpiProgram, poolConfig, "damm config"); - - const uniqueSuffix = `${Date.now()}${Math.floor(Math.random() * 1_000_000) - .toString() - .padStart(6, "0")}`; - - const tokenA = await createSplToken({ - factory: erc20FactoryFromUser, - publicClient, - viem, - owner: deployer, - name: `Router Token A ${uniqueSuffix}`, - symbol: `RTA${uniqueSuffix.slice(-5)}`, - }); - const tokenB = await createSplToken({ - factory: erc20FactoryFromUser, - publicClient, - viem, - owner: deployer, - name: `Router Token B ${uniqueSuffix}`, - symbol: `RTB${uniqueSuffix.slice(-5)}`, - }); - - tokenAMint = tokenA.mintId; - tokenBMint = tokenB.mintId; - tokenAAddress = tokenA.tokenAddress; - tokenBAddress = tokenB.tokenAddress; - tokenAFromUser = tokenA.token; - tokenBFromUser = tokenB.token; - - await assertSolanaAccountExists(cpiProgram, tokenAMint, "token A mint"); - await assertSolanaAccountExists(cpiProgram, tokenBMint, "token B mint"); - - await ensureTokenAccount(tokenAFromUser, publicClient, deployer, "ensure token A account"); - await ensureTokenAccount(tokenBFromUser, publicClient, deployer, "ensure token B account"); - - userTokenAccountA = await tokenAFromUser.read.get_token_account([deployer.account.address]); - userTokenAccountB = await tokenBFromUser.read.get_token_account([deployer.account.address]); - - await assertSolanaAccountExists(cpiProgram, userTokenAccountA, "user token A account"); - await assertSolanaAccountExists(cpiProgram, userTokenAccountB, "user token B account"); - - previewVaultA = await factory.read.previewInitializeVault([tokenAMint, deployer.account.address]); - previewVaultB = await factory.read.previewInitializeVault([tokenBMint, deployer.account.address]); - assert.equal(previewVaultA[1].payer, payer, "vault A payer must match payer"); - assert.equal(previewVaultB[1].payer, payer, "vault B payer must match payer"); - assert.equal(previewVaultA[1].token_mint, tokenAMint, "vault A token mint must match"); - assert.equal(previewVaultB[1].token_mint, tokenBMint, "vault B token mint must match"); - - const mintTokenATxHash = await tokenAFromUser.write.mint_to([deployer.account.address, mintAmount], { - account: deployer.account, - }); - await waitForSuccess(publicClient, mintTokenATxHash, "mint token A"); - - const mintTokenBTxHash = await tokenBFromUser.write.mint_to([deployer.account.address, mintAmount], { - account: deployer.account, - }); - await waitForSuccess(publicClient, mintTokenBTxHash, "mint token B"); - - const initVaultATxHash = await factoryFromUser.write.initializeVaultIfMissing([tokenAMint], { - account: deployer.account, - }); - await waitForSuccess(publicClient, initVaultATxHash, "initialize vault A"); - - const initVaultBTxHash = await factoryFromUser.write.initializeVaultIfMissing([tokenBMint], { - account: deployer.account, - }); - await waitForSuccess(publicClient, initVaultBTxHash, "initialize vault B"); - - await assertSolanaAccountExists(cpiProgram, previewVaultA[1].vault, "vault A"); - await assertSolanaAccountExists(cpiProgram, previewVaultA[1].token_vault, "vault A token vault"); - await assertSolanaAccountExists(cpiProgram, previewVaultA[1].lp_mint, "vault A lp mint"); - await assertSolanaAccountExists(cpiProgram, previewVaultB[1].vault, "vault B"); - await assertSolanaAccountExists(cpiProgram, previewVaultB[1].token_vault, "vault B token vault"); - await assertSolanaAccountExists(cpiProgram, previewVaultB[1].lp_mint, "vault B lp mint"); - - poolPubkey = await factory.read.derivePermissionlessConstantProductPoolWithConfigKey([ - tokenAMint, - tokenBMint, - poolConfig, - ]); - - console.log("Token A Mint: ", tokenAMint); - console.log("Token B Mint: ", tokenBMint); - - const preparedPoolAccounts = await factory.read.preparePermissionlessConstantProductPoolWithConfig2([ - tokenAMint, - tokenBMint, - poolConfig, - ]); - assert.equal( - preparedPoolAccounts.pool.toLowerCase(), - poolPubkey.toLowerCase(), - "prepared pool PDA must match derived pool key", - ); - - const createPoolTxHash = await factoryFromUser.write.createPermissionlessConstantProductPoolWithConfig2( - [ - poolTokenAAmount, - poolTokenBAmount, - preparedPoolAccounts, - ], - { - account: deployer.account, - }, - ); - await waitForSuccess(publicClient, createPoolTxHash, "createPermissionlessConstantProductPoolWithConfig2"); - - console.log("Pool created ", poolPubkey); - await assertSolanaAccountExists(cpiProgram, poolPubkey, "damm v1 pool"); - const poolState = await factory.read.derivePermissionlessConstantProductPoolWithConfigKey([ - tokenAMint, - tokenBMint, - poolConfig, - ]); - assert.equal(poolState.toLowerCase(), poolPubkey.toLowerCase(), "derived pool key must stay stable"); - - const addPoolTxHash = await factoryFromUser.write.addPool([poolPubkey], { - account: deployer.account, - }); - await waitForSuccess(publicClient, addPoolTxHash, "addPool"); - - wrappedPoolAddress = await factory.read.getPool([tokenAAddress, tokenBAddress]); - assert.notEqual(wrappedPoolAddress, zeroAddress, "wrapped pool must be registered"); - - wrappedPool = await viem.getContractAt("ERC20DAMMv1Pool", wrappedPoolAddress, { - client: { - public: publicClient, - wallet: deployer, - }, - }); - internalPool = await viem.getContractAt("DAMMv1Pool", await wrappedPool.read.internal_pool()); - - // Post-2026-05-14 migration: make_balance_liquidity_accounts_from_pool - // takes an EVM address (not the bytes32 PDA). The function derives - // the PDA + ATAs server-side via HelperProgram. Saves ~192K CU per - // liquidity op vs the legacy two-hop derivation pattern. - const liquidityAccounts = await internalPool.read.make_balance_liquidity_accounts_from_pool( - [deployer.account.address], - ); - userPoolLp = liquidityAccounts.user_pool_lp; - }); - - it("executes swapExactTokensForTokens on the router", async function () { - const inputBalanceBefore = await tokenAFromUser.read.balanceOf([deployer.account.address]); - const outputBalanceBefore = await tokenBFromUser.read.balanceOf([deployer.account.address]); - - assert.ok( - inputBalanceBefore >= swapAmountIn, - `swap user must hold at least ${swapAmountIn} token A before swap`, - ); - - const swapTxHash = await routerFromUser.write.swapExactTokensForTokens( - [tokenAAddress, tokenBAddress, swapAmountIn, minAmountOut], - { - account: deployer.account, - }, - ); - await waitForSuccess(publicClient, swapTxHash, "router swap"); - - const inputBalanceAfter = await tokenAFromUser.read.balanceOf([deployer.account.address]); - const outputBalanceAfter = await tokenBFromUser.read.balanceOf([deployer.account.address]); - - assert.equal( - inputBalanceAfter, - inputBalanceBefore - swapAmountIn, - "input token balance must decrease by the swap amount", - ); - assert.ok( - outputBalanceAfter > outputBalanceBefore, - "output token balance must increase after the router swap", - ); - }); - - it("executes addLiquidity on the router", async function () { - const ensurePoolLpTokenAccountTxHash = await routerFromUser.write.ensurePoolLpTokenAccount( - [tokenAAddress, tokenBAddress], - { - account: deployer.account, - }, - ); - await waitForSuccess(publicClient, ensurePoolLpTokenAccountTxHash, "router ensurePoolLpTokenAccount"); - await assertSolanaAccountExists(cpiProgram, userPoolLp, "user pool LP account"); - - const tokenABalanceBefore = await tokenAFromUser.read.balanceOf([deployer.account.address]); - const tokenBBalanceBefore = await tokenBFromUser.read.balanceOf([deployer.account.address]); - const lpBalanceBefore = await readSplTokenAmount(cpiProgram, userPoolLp); - - const quotedPoolTokenAmount = await wrappedPool.read.quoteAddLiquidity([ - addLiquidityTokenAAmount, - addLiquidityTokenBAmount, - 0n, - ]); - - assert.ok(quotedPoolTokenAmount > 0n, "quoted pool token amount must be positive"); - - const addLiquidityTxHash = await routerFromUser.write.addLiquidity( - [ - tokenAAddress, - tokenBAddress, - quotedPoolTokenAmount, - addLiquidityTokenAAmount, - addLiquidityTokenBAmount, - { - user_pool_lp: userPoolLp, - user_a_token: userTokenAccountA, - user_b_token: userTokenAccountB, - }, - ], - { - account: deployer.account, - }, - ); - await waitForSuccess(publicClient, addLiquidityTxHash, "router addLiquidity"); - - const tokenABalanceAfter = await tokenAFromUser.read.balanceOf([deployer.account.address]); - const tokenBBalanceAfter = await tokenBFromUser.read.balanceOf([deployer.account.address]); - const lpBalanceAfter = await readSplTokenAmount(cpiProgram, userPoolLp); - - assert.ok( - tokenABalanceAfter < tokenABalanceBefore, - "token A balance must decrease after addLiquidity", - ); - assert.ok( - tokenBBalanceAfter < tokenBBalanceBefore, - "token B balance must decrease after addLiquidity", - ); - assert.ok( - lpBalanceAfter > lpBalanceBefore, - "LP balance must increase after addLiquidity", - ); - - addedLiquidityLpAmount = lpBalanceAfter - lpBalanceBefore; - assert.ok(addedLiquidityLpAmount > 0n, "minted LP amount must be positive"); - }); - - it("executes removeLiquidity on the router", async function () { - assert.ok(addedLiquidityLpAmount > 0n, "removeLiquidity test requires prior addLiquidity"); - - const tokenABalanceBefore = await tokenAFromUser.read.balanceOf([deployer.account.address]); - const tokenBBalanceBefore = await tokenBFromUser.read.balanceOf([deployer.account.address]); - const lpBalanceBefore = await readSplTokenAmount(cpiProgram, userPoolLp); - - const removeLiquidityTxHash = await routerFromUser.write.removeLiquidity( - [ - tokenAAddress, - tokenBAddress, - addedLiquidityLpAmount, - 0n, - 0n, - ], - { - account: deployer.account, - }, - ); - await waitForSuccess(publicClient, removeLiquidityTxHash, "router removeLiquidity"); - - const tokenABalanceAfter = await tokenAFromUser.read.balanceOf([deployer.account.address]); - const tokenBBalanceAfter = await tokenBFromUser.read.balanceOf([deployer.account.address]); - const lpBalanceAfter = await readSplTokenAmount(cpiProgram, userPoolLp); - - assert.equal( - lpBalanceAfter, - lpBalanceBefore - addedLiquidityLpAmount, - "LP balance must decrease by the removed liquidity amount", - ); - assert.ok( - tokenABalanceAfter > tokenABalanceBefore, - "token A balance must increase after removeLiquidity", - ); - assert.ok( - tokenBBalanceAfter > tokenBBalanceBefore, - "token B balance must increase after removeLiquidity", - ); - }); -}); diff --git a/tests/meteora_vault_parser.test.ts b/tests/meteora_vault_parser.test.ts deleted file mode 100644 index 6ddfde9..0000000 --- a/tests/meteora_vault_parser.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Unit test pinning DAMMv1Lib.VAULT_MIN_LEN to the parser's actual byte -// consumption. The constant is used in two ways: -// 1. parse_vault's prefix-length sanity check -// 2. The size of the slice load_vault requests from account_data_at -// If (1) is smaller than what parse_vault actually advances through, the -// slice is too short and parse_vault reverts "oob u64" when it reads past -// the slice end. That's exactly what broke on the freshly-deployed Meteora -// factory `0xa3a4a275…` on Hadrian, 2026-05-18. -// -// parse_vault offsets (from damm_v1_pool.sol::parse_vault): -// 8 anchor discriminator -// +3 enabled (u8) + vault_bump (u8) + token_vault_bump (u8) -// +8 total_amount (u64) -// +128 4× bytes32 (token_vault, fee_vault, token_mint, lp_mint) -// +960 strategies[30] (32 × 30) -// +96 base + admin + operator (32 × 3) -// +24 3× u64 locked_profit_tracker fields -// =1227 bytes -import { before, describe, it } from "node:test"; -import assert from "node:assert/strict"; -import hardhat from "hardhat"; - -// Total bytes parse_vault actually advances through. -const PARSE_VAULT_CONSUMPTION = 1227; - -function zeroBuffer(len: number): `0x${string}` { - return ("0x" + "00".repeat(len)) as `0x${string}`; -} - -describe("DAMMv1Lib.parse_vault — VAULT_MIN_LEN bounds", function () { - let harness: any; - let minLen: bigint; - - before(async function () { - const { viem } = await hardhat.network.connect(); - harness = await viem.deployContract("DAMMv1VaultParserHarness", []); - minLen = await harness.read.vaultMinLen(); - }); - - it("VAULT_MIN_LEN must be >= what parse_vault consumes (1227 bytes)", function () { - assert.ok( - Number(minLen) >= PARSE_VAULT_CONSUMPTION, - `VAULT_MIN_LEN=${minLen} is smaller than parse_vault's actual byte consumption ` + - `(${PARSE_VAULT_CONSUMPTION}). load_vault's slice will be too short and parse_vault will revert "oob u64".` - ); - }); - - it("parse_vault must succeed on a buffer of length VAULT_MIN_LEN", async function () { - const buf = zeroBuffer(Number(minLen)); - // No field-level validation — just byte advancement. Must not revert. - await harness.read.parseVault([buf]); - }); - - it("parse_vault must revert on a buffer shorter than VAULT_MIN_LEN", async function () { - const buf = zeroBuffer(Number(minLen) - 1); - await assert.rejects( - () => harness.read.parseVault([buf]), - /InvalidVaultDataLength|reverted/i, - "parse_vault should reject buffers smaller than VAULT_MIN_LEN" - ); - }); -});