├── .github └── workflows │ ├── audit.yaml │ ├── build.yaml │ └── generate-sdk.yaml ├── .gitignore ├── .prettierignore ├── .solitarc.js ├── .yarnrc.yml ├── Anchor.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── migrations └── deploy.ts ├── package.json ├── programs └── solvent-protocol │ ├── Cargo.toml │ ├── Xargo.toml │ ├── src │ ├── common.rs │ ├── constants.rs │ ├── errors.rs │ ├── instructions │ │ ├── claim_balance.rs │ │ ├── create_bucket.rs │ │ ├── deposit_nft.rs │ │ ├── liquidate_locker.rs │ │ ├── lock_nft.rs │ │ ├── migrate_droplets.rs │ │ ├── migrate_nft.rs │ │ ├── mod.rs │ │ ├── redeem_nft.rs │ │ ├── set_locking_enabled.rs │ │ ├── set_staking_enabled.rs │ │ ├── stake_nft.rs │ │ ├── start_migration.rs │ │ ├── unlock_nft.rs │ │ ├── unstake_nft.rs │ │ ├── update_collection_info.rs │ │ ├── update_locking_params.rs │ │ └── update_staking_params.rs │ ├── lib.rs │ └── state.rs │ └── tests │ └── tests.rs ├── rust-toolchain ├── tests ├── genesis-programs │ ├── gem_bank.so │ ├── gem_farm.so │ ├── mpl_token_metadata.so │ └── solvent.so ├── idls │ ├── gem_bank.json │ ├── gem_farm.json │ └── solvent.json ├── keypairs │ └── solvent-admin.json ├── nft-mints │ └── smb.json ├── tests │ ├── buckets │ │ ├── create-bucket.ts │ │ └── update-collection-info.ts │ ├── common.ts │ ├── deposit-nfts │ │ ├── deposit-nfts.ts │ │ ├── redeem-nfts.ts │ │ └── swap-nfts.ts │ ├── lock-nfts │ │ ├── liquidate-locker.ts │ │ ├── lock-nfts.ts │ │ ├── unlock-nfts.ts │ │ └── update-locking-params.ts │ ├── migration │ │ ├── migrate-droplets.ts │ │ ├── migrate-nft.ts │ │ └── start-migration.ts │ ├── misc │ │ └── claim-balance.ts │ └── staking │ │ ├── stake-nft.ts │ │ ├── unstake-nft.ts │ │ └── update-staking-params.ts ├── types │ └── solvent.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.github/workflows/audit.yaml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | on: 3 | issue_comment: 4 | types: [created, edited] 5 | 6 | jobs: 7 | sec3-audit: 8 | if: | 9 | github.event.issue.pull_request && 10 | contains(github.event.comment.body, '~audit') && 11 | contains(fromJson('["skulltech", "mdhrumil", "exogenesys"]'), github.event.comment.user.login) 12 | 13 | name: Run Sec3 Pro automated audit 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Get PR details 18 | uses: xt0rted/pull-request-comment-branch@v1 19 | id: comment-branch 20 | - name: Set commit status as pending 21 | uses: myrotvorets/set-commit-status-action@master 22 | with: 23 | sha: ${{ steps.comment-branch.outputs.head_sha }} 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | status: pending 26 | 27 | - uses: actions/checkout@v3 28 | with: 29 | ref: ${{ steps.comment-branch.outputs.head_ref }} 30 | - name: Sec3 Pro Audit 31 | continue-on-error: false 32 | uses: sec3dev/pro-action@v1 33 | with: 34 | sec3-token: ${{ secrets.SEC3_TOKEN }} 35 | path: programs/solvent-protocol 36 | 37 | - name: Set final commit status 38 | uses: myrotvorets/set-commit-status-action@master 39 | if: always() 40 | with: 41 | sha: ${{ steps.comment-branch.outputs.head_sha }} 42 | token: ${{ secrets.GITHUB_TOKEN }} 43 | status: ${{ job.status }} 44 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | issue_comment: 4 | types: [created, edited] 5 | 6 | jobs: 7 | build: 8 | if: | 9 | github.event.issue.pull_request && 10 | contains(github.event.comment.body, '~build') && 11 | contains(fromJson('["skulltech", "mdhrumil", "exogenesys"]'), github.event.comment.user.login) 12 | 13 | name: Format code and run tests 14 | runs-on: ubuntu-latest 15 | container: projectserum/build:v0.24.2 16 | 17 | steps: 18 | - name: Get PR details 19 | uses: xt0rted/pull-request-comment-branch@v1 20 | id: comment-branch 21 | - name: Set commit status as pending 22 | uses: myrotvorets/set-commit-status-action@master 23 | with: 24 | sha: ${{ steps.comment-branch.outputs.head_sha }} 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | status: pending 27 | 28 | - name: Upgrade Git 29 | run: | 30 | apt-get update -y 31 | apt-get install software-properties-common -y 32 | add-apt-repository ppa:git-core/ppa -y 33 | apt-get update -y 34 | apt-get install git -y 35 | git --version 36 | git config --global --add safe.directory '*' 37 | - uses: actions/checkout@v3 38 | with: 39 | ref: ${{ steps.comment-branch.outputs.head_ref }} 40 | - name: Install Node dependencies 41 | run: yarn install 42 | - name: Cache Rust dependencies 43 | uses: Swatinem/rust-cache@v1 44 | - name: Create a new Solana keypair 45 | run: solana-keygen new -s --no-bip39-passphrase 46 | - name: Format and lint code 47 | run: yarn format 48 | - name: Run tests 49 | run: yarn test 50 | - name: Commit changes 51 | uses: EndBug/add-and-commit@v9 52 | with: 53 | default_author: github_actions 54 | 55 | - name: Set final commit status 56 | uses: myrotvorets/set-commit-status-action@master 57 | if: always() 58 | with: 59 | sha: ${{ steps.comment-branch.outputs.head_sha }} 60 | token: ${{ secrets.GITHUB_TOKEN }} 61 | status: ${{ job.status }} 62 | -------------------------------------------------------------------------------- /.github/workflows/generate-sdk.yaml: -------------------------------------------------------------------------------- 1 | name: Generate SDK 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | generate-sdk: 9 | name: Generate TypeScript SDK and docs 10 | runs-on: ubuntu-latest 11 | container: projectserum/build:v0.24.2 12 | 13 | steps: 14 | - name: Upgrade Git 15 | run: | 16 | apt-get update -y 17 | apt-get install software-properties-common -y 18 | add-apt-repository ppa:git-core/ppa -y 19 | apt-get update -y 20 | apt-get install git -y 21 | git --version 22 | git config --global --add safe.directory '*' 23 | - name: Checkout program repo 24 | uses: actions/checkout@v3 25 | with: 26 | path: solvent-program 27 | - name: Install Node dependencies 28 | run: yarn install 29 | working-directory: ./solvent-program 30 | - name: Checkout SDK repo 31 | uses: actions/checkout@v3 32 | with: 33 | repository: solventprotocol/solvent-sdk 34 | ref: master 35 | path: solvent-sdk 36 | token: ${{ secrets.CI_PAT }} 37 | - name: Install Node dependencies 38 | run: yarn install 39 | working-directory: ./solvent-sdk 40 | - name: Cache Rust dependencies 41 | uses: Swatinem/rust-cache@v1 42 | with: 43 | working-directory: ./solvent-program 44 | - name: Generate TypeScript SDK using Solita 45 | run: yarn solita 46 | working-directory: ./solvent-program 47 | - name: Generate docs using TypeDoc 48 | run: yarn typedoc 49 | working-directory: ./solvent-sdk 50 | - name: Bump version and publish SDK to NPM 51 | run: | 52 | echo "_authToken=${NPM_TOKEN}" > ~/.npmrc 53 | yarn ci-p 54 | working-directory: ./solvent-sdk 55 | env: 56 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | - name: Commit changes 58 | uses: EndBug/add-and-commit@v9 59 | with: 60 | cwd: ./solvent-sdk 61 | default_author: github_actions 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .DS_Store 3 | target 4 | docker-target 5 | node_modules 6 | .crates 7 | .vscode 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | target 2 | sdk 3 | programs 4 | tests/types 5 | *.json 6 | -------------------------------------------------------------------------------- /.solitarc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const os = require("os"); 3 | const isCI = require("is-ci"); 4 | 5 | const programDir = path.join(__dirname, "programs", "solvent-protocol"); 6 | const idlDir = path.join(__dirname, "target", "idl"); 7 | const sdkDir = path.join(__dirname, "../solvent-sdk", "src"); 8 | const binaryInstallDir = path.join(isCI ? "/root" : os.homedir(), ".cargo"); 9 | 10 | module.exports = { 11 | idlGenerator: "anchor", 12 | programName: "solvent_protocol", 13 | programId: "SVTy4zMgDPExf1RaJdoCo5HvuyxrxdRsqF1uf2Rcd7J", 14 | idlDir, 15 | sdkDir, 16 | binaryInstallDir, 17 | programDir, 18 | }; 19 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | # [programs.mainnet] 2 | # solvent_protocol = "SVTy4zMgDPExf1RaJdoCo5HvuyxrxdRsqF1uf2Rcd7J" 3 | 4 | [programs.localnet] 5 | solvent_protocol = "SVTy4zMgDPExf1RaJdoCo5HvuyxrxdRsqF1uf2Rcd7J" 6 | 7 | [[test.genesis]] 8 | address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" 9 | program = "./tests/genesis-programs/mpl_token_metadata.so" 10 | 11 | [[test.genesis]] 12 | address = "nft3agWJsaL1nN7pERYDFJUf54BzDZwS3oRbEzjrq6q" 13 | program = "./tests/genesis-programs/solvent.so" 14 | 15 | [[test.genesis]] 16 | address = "bankHHdqMuaaST4qQk6mkzxGeKPHWmqdgor6Gs8r88m" 17 | program = "./tests/genesis-programs/gem_bank.so" 18 | 19 | [[test.genesis]] 20 | address = "farmL4xeBFVXJqtfxCzU9b28QACM7E2W2ctT6epAjvE" 21 | program = "./tests/genesis-programs/gem_farm.so" 22 | 23 | [registry] 24 | url = "https://anchor.projectserum.com" 25 | 26 | [provider] 27 | cluster = "localnet" 28 | wallet = "~/.config/solana/id.json" 29 | 30 | # [provider] 31 | # cluster = "https://ssc-dao.genesysgo.net/" 32 | # wallet = "~/.config/solana/mainnet.json" 33 | 34 | [scripts] 35 | test = "yarn run ts-mocha --parallel -t 1000000 tests/**/*.ts" 36 | 37 | [features] 38 | seeds = true 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 |

Changelog

2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.3.0] - 2022-09-29 8 | 9 | ### Added 10 | 11 | - Block stolen NFTs from Solvent by checking if its `sellerFeeBasisPoints >= 9500` ([#28](https://github.com/solventprotocol/solvent-program/pull/28), [#34](https://github.com/solventprotocol/solvent-program/pull/34)). 12 | - Added `modify_collection_info` admin instruction for modifying collection info of a bucket ([#29](https://github.com/solventprotocol/solvent-program/pull/29)). 13 | 14 | ### Changes 15 | 16 | - Emit more info in events ([#31](https://github.com/solventprotocol/solvent-program/pull/31)). 17 | 18 | ## [1.2.0] - 2022-09-20 19 | 20 | ### Added 21 | 22 | - 10% of the platform fees are sent to the distributor. The distributor should pass its address to the `redeem_nft` instruction ([#24](https://github.com/solventprotocol/solvent-program/pull/24)). 23 | - Emit all data related to the instruction calls in events ([#23](https://github.com/solventprotocol/solvent-program/pull/23)). 24 | 25 | ### Changes 26 | 27 | - Some of `migrate_droplets` instruction's input accounts are renamed ([#25](https://github.com/solventprotocol/solvent-program/pull/25)). 28 | 29 | ### Fixes 30 | 31 | - Audit by OtterSec and Soteria, fix related bugs ([#26](https://github.com/solventprotocol/solvent-program/pull/24), [#27](https://github.com/solventprotocol/solvent-program/pull/24)). 32 | 33 | ## [1.1.0] - 2022-08-20 34 | 35 | ### Added 36 | 37 | - Added platform fees: 2% for withdrawing NFTs and 0.5% for swapping NFTs ([#17](https://github.com/solventprotocol/solvent-program/pull/17), [#18](https://github.com/solventprotocol/solvent-program/pull/18), [#19](https://github.com/solventprotocol/solvent-program/pull/19), [#21](https://github.com/solventprotocol/solvent-program/pull/21)). 38 | - Added `swap_nfts` instruction for swapping NFTs. A new `SwapState` PDA is created for each ongoing swap which stores whether user has deposited an NFT for swapping with another one ([#20](https://github.com/solventprotocol/solvent-program/pull/20)). 39 | 40 | ### Changes 41 | 42 | - Renamed some input account names ([#15](https://github.com/solventprotocol/solvent-program/pull/15)). 43 | 44 | ## [1.0.0] - 2022-07-05 45 | 46 | Initial open-source release. 47 | 48 | ### Added 49 | 50 | - Tokenizing NFTs to droplets. 51 | - Instant NFT loans using NFT lockers. 52 | - Auto-staking of NFTs in bucket to [gemfarm](https://github.com/gemworks/gem-farm) staking pools. 53 | - Audit by OtterSec. 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The solvent-program repository follows the Rust [Code of Conduct](https://www.rust-lang.org/conduct.html). 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | 6 | [profile.release] 7 | overflow-checks = true 8 | lto = "fat" 9 | codegen-units = 1 10 | 11 | [profile.release.build-override] 12 | opt-level = 3 13 | incremental = false 14 | codegen-units = 1 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Solvent Protocol - Financializing NFTs on Solana

5 |
6 | 7 | Solvent is a protocol to convert the NFTs into a fixed number of fungible tokens of that collection. 8 | 9 | - These fungible tokens a.k.a. droplets, are tradable on Serum orderbooks and AMMs across Solana 10 | - 1 NFT --> 100 droplets --> 1 NFT 11 | - Droplets are unique to each collection, and every NFT mints the same number of droplets of that collection. 12 | - Droplets enables instant liquidity, and DeFi applications such as LPing, lending, and long/shorts to illiquid NFTs. 13 | - Read our litepaper [here](https://drive.google.com/file/d/1HwybfOLGkQ_HAo5zBl2Hf19yTnz4rtQH/view). 14 | - NFT lockers is a functionality to mint depositor some droplets as a debt but locking that NFT so that it is not available in the open bucket. User can claim their locked NFTs by unlocking them and paying back the droplets debt + some interest within a duration. Failure to unlock the NFT within the duration specified results in locked NFT getting liquidated. 15 | 16 | > Solvent Protocol smart contracts are audited by the amazing team at [Otter Sec](https://osec.io/). Check out the audit report [here](https://drive.google.com/file/d/1tVe-0EQhslQxr56nOQmxqaB8BSDwJ1s1/view). 17 | > 18 | > Solvent contracts are also using [Sec3](https://sec3.dev/)'s premium auto-auditor for vulnerability checks and scans. Check out the report [here](https://drive.google.com/file/d/1WleYAar8t2ZXRIpINjH2D8fXxuXhG4A6/view?usp=sharing). 19 | 20 | ## Protocol functionality 21 | 22 | These are the following functionality that the users can perform with the protocol: 23 | 24 | - Create a bucket on Solvent for an NFT collection 25 | - Mint 100 droplets by depositing the NFT into its suitable bucket 26 | - Burn 100 droplets to claim an NFT from the bucket 27 | 28 | ### NFT Lockers 29 | 30 | NFT lockers are vaults that users can use to lock their NFT for a certain duration **D** into Solvent and unlock instant liquidity for them. Within that duration, the users can pay back the debt and interest and can claim their NFT back. Users can select any duration up to a certain duration limit **Dmax**. The duration can be mentioned in the number of hours. If the users default to make the payment of debt and interest, they’re liquidated. If a user unlocks the NFT before they’re about to be liquidated, they only pay interest for the duration for which the NFT was locked. The interest that is paid is accumulated in an interest vault account. 31 | 32 | The formula for droplets minted for an NFT that gets locked is shown as below: 33 | 34 |
35 | 36 |
37 | 38 | The formula for the interest that the user ends up paying for unlocking the NFT is shown as below: 39 | 40 |
41 | 42 |
43 | 44 | ## Function calls 45 | 46 | | Function | Description | 47 | | :----------------- | :---------------------------------------------------------------- | 48 | | `create_bucket` | Create a bucket for an NFT collection on Solvent | 49 | | `deposit_nft` | Deposit an NFT into a bucket to mint 100 droplets | 50 | | `redeem_nft` | Burn 100 droplets to claim the NFT from the bucket | 51 | | `lock_nft` | Lock an NFT for a duration to mint droplets as a debt | 52 | | `unlock_nft` | Unlock a locked NFT by paying back the droplets debt + interest | 53 | | `liquidate_locker` | Liquidate a locked NFT that is overdue unlocking by the depositor | 54 | 55 | ## State structs 56 | 57 | ### BucketStates 58 | 59 | #### BucketStateV3 60 | 61 | PDA to store the details of an NFT collection details on-chain. Represents a unique bucket for an NFT collection. 62 | What's new: 63 | 64 | - The verification details such as NFT symbol in the metadata + list of verified creators is now stored in a separate enum named CollectionInfo. CollectionInfo also accounts for cases where the NFT collection can be represented using a unique collectionMint as per Metaplex v1.1 metadata standard. 65 | - Additional variables required for NFT lockers 66 | 67 | | Fields | Description | Type | 68 | | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------- | 69 | | `bump` | The canonical bump for the BucketStateV3 PDA | u8 | 70 | | `droplet_mint` | The public key address of the droplet token for the NFT collection | Pubkey | 71 | | `collection_info` | The enum value representing verification details of the NFT collection | CollectionInfo | 72 | | `is_lockers_enabled` | A boolean variable representing whether NFT lockers feature is enabled for a bucket of an NFT collection. False by default. Can only be updated by Solvent team. Decentralized in the future. | bool | 73 | | `max_locker_duration` | The max duration in seconds for which an NFT can be locked into the locker of this bucket | u64 | 74 | | `num_nfts_in_bucket` | The number of NFTs deposited into the bucket | u16 | 75 | | `num_nfts_in_locker` | The number of NFTs locked into a locker of the bucket | u16 | 76 | | `interest_scaler` | The % value multiplier to scale the interest payable for NFT lockers functionality. Range: (0,100] | u8 | 77 | 78 | ### DepositState 79 | 80 | PDA to store the details of an NFT that is deposited into a bucket. Every NFT deposited into Solvent will now have its own on-chain state representing the deposit transaction. 81 | 82 | | Fields | Description | Type | 83 | | :------------- | :----------------------------------------------------------------- | :----- | 84 | | `bump` | The canonical bump for the DepositState PDA | u8 | 85 | | `droplet_mint` | The public key address of the droplet token for the NFT collection | Pubkey | 86 | | `nft_mint` | The public key address of the NFT asset deposited into the bucket | String | 87 | 88 | ### LockerState 89 | 90 | PDA to store the details of an NFT that is locked into a locker of the bucket. Every NFT locked into lockers of the bucket will have its own on-chain state representing the locked transaction. 91 | 92 | | Fields | Description | Type | 93 | | :--------------------- | :----------------------------------------------------------------------- | :----- | 94 | | `droplet_mint` | The public key address of the droplet token for the NFT collection | Pubkey | 95 | | `depositor` | The public key address of the original user who has locked the NFT asset | Pubkey | 96 | | `nft_mint` | The public key address of the NFT asset deposited into the bucket | String | 97 | | `creation_timestamp` | The unix timestamp when the NFT got locked into the locker | u64 | 98 | | `duration` | The duration in seconds for which the NFT is locked into the locker | u64 | 99 | | `principal_amount` | The amount of droplets minted to the user as a debt for locking the NFT | u64 | 100 | | `max_interest_payable` | The maximum interest payable by the depositor while unlocking the NFT | u64 | 101 | 102 | ## Queries 103 | 104 | Thank you for your interest in Solvent smart-contracts! 105 | Please email us at: [mmdhrumil@solventprotocol.com](emailto:mmdhrumil@solventprotocol.com) for queries and more. 106 | 107 | ### Thanks ❤️ 108 | -------------------------------------------------------------------------------- /migrations/deploy.ts: -------------------------------------------------------------------------------- 1 | // Migrations are an early feature. Currently, they're nothing more than this 2 | // single deploy script that's invoked from the CLI, injecting a provider 3 | // configured from the workspace's Anchor.toml. 4 | 5 | import { web3 } from "@project-serum/anchor"; 6 | import { readFileSync } from "fs"; 7 | import { resolve } from "path"; 8 | 9 | const anchor = require("@project-serum/anchor"); 10 | const { TOKEN_PROGRAM_ID } = require("@solana/spl-token"); 11 | 12 | module.exports = async function (provider) { 13 | // Configure client to use the provider. 14 | anchor.setProvider(provider); 15 | 16 | // Read the generated IDL. 17 | const idl = JSON.parse( 18 | readFileSync("../target/idl/solvent_protocol.json", "utf8") 19 | ); 20 | 21 | // Address of the deployed program. 22 | const programId = new anchor.web3.PublicKey( 23 | "SVTy4zMgDPExf1RaJdoCo5HvuyxrxdRsqF1uf2Rcd7J" 24 | ); 25 | 26 | const program = new anchor.Program(idl, programId); 27 | 28 | const SOLVENT_ADMIN: web3.Keypair = web3.Keypair.fromSecretKey( 29 | Buffer.from( 30 | JSON.parse( 31 | readFileSync( 32 | resolve(__dirname, "../target/deploy", "solvent-admin.json"), 33 | "utf-8" 34 | ) 35 | ) 36 | ) 37 | ); 38 | 39 | const bucketInfos = [ 40 | { 41 | collectionName: "Aurory", 42 | dropletSymbol: "AUR", 43 | dropletMintOld: new anchor.web3.PublicKey( 44 | "GUtWHTy9N5Av4LTB5PJPn4ZTfxCB2tGiK7DJpS7y8F8S" 45 | ), 46 | dropletMintNew: new anchor.web3.PublicKey( 47 | "HYtdDGdMFqBrtyUe5z74bKCtH2WUHZiWRicjNVaHSfkg" 48 | ), 49 | }, 50 | { 51 | collectionName: "Balloonsville", 52 | dropletSymbol: "BV", 53 | dropletMintOld: new anchor.web3.PublicKey( 54 | "AgBQSKgZPJPmsMz8qkHbyZdEU4JrRpoNHYh2kxE5TcD1" 55 | ), 56 | dropletMintNew: new anchor.web3.PublicKey( 57 | "CT1iZ7MJzm8Riy6MTgVht2PowGetEWrnq1SfmUjKvz8c" 58 | ), 59 | }, 60 | { 61 | collectionName: "Degen Ape Academy", 62 | dropletSymbol: "DAPE", 63 | dropletMintOld: new anchor.web3.PublicKey( 64 | "dapeM1DJj3xf2rC5o3Gcz1Cg3Rdu2ayZae9nGcsRRZT" 65 | ), 66 | dropletMintNew: new anchor.web3.PublicKey( 67 | "6F5A4ZAtQfhvi3ZxNex9E1UN5TK7VM2enDCYG1sx1AXT" 68 | ), 69 | }, 70 | { 71 | collectionName: "DeGods", 72 | dropletSymbol: "DGOD", 73 | dropletMintOld: new anchor.web3.PublicKey( 74 | "DpcmtJniwGRPqU6A8shdcV812uddwoxDCMfXUz2SkLVJ" 75 | ), 76 | dropletMintNew: new anchor.web3.PublicKey( 77 | "DCgRa2RR7fCsD63M3NgHnoQedMtwH1jJCwZYXQqk9x3v" 78 | ), 79 | }, 80 | { 81 | collectionName: "Famous Fox Federation", 82 | dropletSymbol: "FFF", 83 | dropletMintOld: new anchor.web3.PublicKey( 84 | "2Kc91qK5tA1df2P9BWrjNrJfdQCbCx95iUY8H27aNuWa" 85 | ), 86 | dropletMintNew: new anchor.web3.PublicKey( 87 | "BoeDfSFRyaeuaLP97dhxkHnsn7hhhes3w3X8GgQj5obK" 88 | ), 89 | }, 90 | { 91 | collectionName: "Galactic Geckos", 92 | dropletSymbol: "GGSG", 93 | dropletMintOld: new anchor.web3.PublicKey( 94 | "ggsgHDoX6tACq25XhQPUmbza8Fzwp9WdAzTU1voTwDi" 95 | ), 96 | dropletMintNew: new anchor.web3.PublicKey( 97 | "3GQqCi9cuGhAH4VwkmWD32gFHHJhxujurzkRCQsjxLCT" 98 | ), 99 | }, 100 | { 101 | collectionName: "Genopets Genesis", 102 | dropletSymbol: "GENO", 103 | dropletMintOld: new anchor.web3.PublicKey( 104 | "GknXZXR3Y84wgmDUxtsoR9FBHEZovqXFuDK2jczi1ydz" 105 | ), 106 | dropletMintNew: new anchor.web3.PublicKey( 107 | "4MSMKZwGnkT8qxK8LsdH28Uu8UfKRT2aNaGTU8TEMuHz" 108 | ), 109 | }, 110 | { 111 | collectionName: "Gooney Toons", 112 | dropletSymbol: "GOON", 113 | dropletMintOld: new anchor.web3.PublicKey( 114 | "goonVPLC3DARhntpoE56nrsybwMnP76St5JoywzCmMw" 115 | ), 116 | dropletMintNew: new anchor.web3.PublicKey( 117 | "9acdc5M9F9WVM4nVZ2gPtVvkeYiWenmzLW9EsTkKdsUJ" 118 | ), 119 | }, 120 | { 121 | collectionName: "Honey Genesis Bee", 122 | dropletSymbol: "HNYG", 123 | dropletMintOld: new anchor.web3.PublicKey( 124 | "GuRdDYCNuykG28e77aFVD7gvwdeRqziBfQYdCdRqSVVS" 125 | ), 126 | dropletMintNew: new anchor.web3.PublicKey( 127 | "DXA9itWDGmGgqqUoHnBhw6CjvJKMUmTMKB17hBuoYkfQ" 128 | ), 129 | }, 130 | { 131 | collectionName: "Lifinity Flares", 132 | dropletSymbol: "LIFL", 133 | dropletMintOld: new anchor.web3.PublicKey( 134 | "5aGsu5hASnsnQVXV58fN8Jw9P8BVyfDnH2eYatmFLGoQ" 135 | ), 136 | dropletMintNew: new anchor.web3.PublicKey( 137 | "4wGimtLPQhbRT1cmKFJ7P7jDTgBqDnRBWsFXEhLoUep2" 138 | ), 139 | }, 140 | { 141 | collectionName: "Pesky Penguins", 142 | dropletSymbol: "PSK", 143 | dropletMintOld: new anchor.web3.PublicKey( 144 | "pskJRUNzJbVu4RaZSUJYfvaTNXmFdRCutegL6P2Y9tG" 145 | ), 146 | dropletMintNew: new anchor.web3.PublicKey( 147 | "Bp6k6xacSc4KJ5Bmk9D5xfbw8nN42ZHtPAswEPkNze6U" 148 | ), 149 | }, 150 | { 151 | collectionName: "Playground Epoch", 152 | dropletSymbol: "EPOCH", 153 | dropletMintOld: new anchor.web3.PublicKey( 154 | "epchejN2prm44RwMfWetBkbMr4wnEXHmMN9nmKyx2TQ" 155 | ), 156 | dropletMintNew: new anchor.web3.PublicKey( 157 | "3b9wtU4VP6qSUDL6NidwXxK6pMvYLFUTBR1QHWCtYKTS" 158 | ), 159 | }, 160 | { 161 | collectionName: "Playground Waves", 162 | dropletSymbol: "PLWAV", 163 | dropletMintOld: new anchor.web3.PublicKey( 164 | "FQkm6bACFuJpGDmnkvYoq2Luhcc65oam2dg1tXfnKWAY" 165 | ), 166 | dropletMintNew: new anchor.web3.PublicKey( 167 | "8vkTew1mT8w5NapTqpAoNUNHW2MSnAGVNeu8QPmumSJM" 168 | ), 169 | }, 170 | { 171 | collectionName: "Solana Monkey Business", 172 | dropletSymbol: "PLWAV", 173 | dropletMintOld: new anchor.web3.PublicKey( 174 | "smbdJcLBrtKPhjrWCpDv5ABdJwz2vYo3mm6ojmePL3t" 175 | ), 176 | dropletMintNew: new anchor.web3.PublicKey( 177 | "Ca5eaXbfQQ6gjZ5zPVfybtDpqWndNdACtKVtxxNHsgcz" 178 | ), 179 | }, 180 | { 181 | collectionName: "Catalina Whale Mixer", 182 | dropletSymbol: "CWM", 183 | dropletMintOld: new anchor.web3.PublicKey( 184 | "cwmkTPCxDkYpBjLQRNhcCKxuxnAQW6ahS7JQLeXrsXt" 185 | ), 186 | dropletMintNew: new anchor.web3.PublicKey( 187 | "8W2ZFYag9zTdnVpiyR4sqDXszQfx2jAZoMcvPtCSQc7D" 188 | ), 189 | }, 190 | { 191 | collectionName: "Thugbirdz", 192 | dropletSymbol: "THUGZ", 193 | dropletMintOld: new anchor.web3.PublicKey( 194 | "FFBnqafsjrvvxxf5n3Tzba8V7vWPb8wr5DPEog1VAwff" 195 | ), 196 | dropletMintNew: new anchor.web3.PublicKey( 197 | "EmLJ8cNEsUtboiV2eiD6VgaEscSJ6zu3ELhqixUP4J56" 198 | ), 199 | }, 200 | { 201 | collectionName: "Visionary Studios", 202 | dropletSymbol: "VSNRY", 203 | dropletMintOld: new anchor.web3.PublicKey( 204 | "8k8nYi4NSigPgk9CijcDJyoraGr273AggWZFgn8Adk1a" 205 | ), 206 | dropletMintNew: new anchor.web3.PublicKey( 207 | "EiasWmzy9MrkyekABHLfFRkGhRakaWNvmQ8h5DV86zyn" 208 | ), 209 | }, 210 | ]; 211 | 212 | for (const bucketInfo of bucketInfos) { 213 | const dropletMintOld = bucketInfo.dropletMintOld; 214 | const dropletMintNew = bucketInfo.dropletMintNew; 215 | 216 | console.log("Enabling migration"); 217 | console.log("dropletMintOld: ", dropletMintOld.toString()); 218 | console.log("dropletMintNew: ", dropletMintNew.toString()); 219 | 220 | let res = await program.methods 221 | .startMigration() 222 | .accounts({ 223 | signer: SOLVENT_ADMIN.publicKey, 224 | dropletMintOld, 225 | dropletMintNew, 226 | }) 227 | .signers([SOLVENT_ADMIN]) 228 | .rpc(); 229 | } 230 | }; 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solvent-protocol", 3 | "scripts": { 4 | "test": "cargo test && anchor test -- --features test-ids", 5 | "format": "cargo fmt --all && cargo clippy && prettier '.' --write" 6 | }, 7 | "dependencies": { 8 | "@project-serum/anchor": "^0.24.2" 9 | }, 10 | "devDependencies": { 11 | "@gemworks/gem-farm-ts": "^0.24.1", 12 | "@metaplex-foundation/mpl-token-metadata": "^2.1.1", 13 | "@metaplex-foundation/solita": "^0.6.0", 14 | "@project-serum/anchor-cli": "^0.24.2", 15 | "@solana/spl-token-latest": "npm:@solana/spl-token", 16 | "@types/chai": "^4.3.0", 17 | "@types/mocha": "^9.1.0", 18 | "chai": "^4.3.6", 19 | "is-ci": "^3.0.1", 20 | "keccak256": "^1.0.6", 21 | "merkletreejs": "^0.2.31", 22 | "mocha": "^9.2.2", 23 | "prettier": "^2.6.2", 24 | "ts-mocha": "^9.0.2", 25 | "typescript": "^4.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /programs/solvent-protocol/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solvent-protocol" 3 | version = "1.0.0" 4 | description = "Earn yield with your NFTs and enable instant NFT trades and loans" 5 | license = "AGPL-3.0-only" 6 | homepage = "https://solvent.xyz/" 7 | repository = "https://github.com/solventprotocol/solvent-program/" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "lib"] 12 | name = "solvent_protocol" 13 | 14 | [features] 15 | no-entrypoint = [] 16 | no-idl = [] 17 | cpi = ["no-entrypoint"] 18 | default = [] 19 | test-ids = [] 20 | 21 | [dependencies] 22 | anchor-lang = { version = "0.24.2", features = ["init-if-needed"] } 23 | anchor-spl = "0.24.2" 24 | solana-program = "~1.9.13" 25 | mpl-token-metadata = { version = "1.2.7", features = ["no-entrypoint"] } 26 | gem_farm = { git = "https://github.com/solventprotocol/gem-farm", branch = "main", features=["cpi"] } 27 | gem_bank = { git = "https://github.com/solventprotocol/gem-farm", branch = "main", features=["cpi"] } 28 | 29 | [dev-dependencies] 30 | test-case = "2.0.2" 31 | assert-panic = "1.0.1" 32 | -------------------------------------------------------------------------------- /programs/solvent-protocol/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/common.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::*; 2 | use crate::errors::SolventError; 3 | use anchor_lang::prelude::*; 4 | use gem_farm::state::{Farm, Farmer}; 5 | use mpl_token_metadata::state::Metadata; 6 | 7 | // Collection info, required to verify if an NFT belongs to a collection 8 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)] 9 | pub enum CollectionInfo { 10 | // Symbol and verified creators of the collection, for metadata accounts created by CreateMetadataAccount 11 | V1 { 12 | symbol: String, 13 | verified_creators: Vec, 14 | whitelist_root: [u8; 32], 15 | }, 16 | // The token mint of the collection NFT, for metadata accounts created by CreateMetadataAccountV2 17 | V2 { 18 | collection_mint: Pubkey, 19 | }, 20 | } 21 | 22 | impl CollectionInfo { 23 | // 1 + largest variant: 1 String of 8 chars, 1 Vev, 1 hash of 32 bytes 24 | pub const LEN: usize = 1 + (4 + 32) + (4 + (32 * 5)) + 32; 25 | } 26 | 27 | // Validate if a collection info is valid 28 | pub fn validate_collection_info(collection_info: &CollectionInfo) -> Result<()> { 29 | match collection_info { 30 | CollectionInfo::V1 { 31 | ref symbol, 32 | ref verified_creators, 33 | whitelist_root: _, 34 | } => { 35 | // Check if symbol is too long 36 | require!( 37 | // Max string length is 8, so UTF-8 encoded max byte length is 32 38 | symbol.len() <= 8 * 4, 39 | SolventError::CollectionSymbolInvalid 40 | ); 41 | 42 | // Check if there are 1-5 verified creators 43 | require!( 44 | !verified_creators.is_empty() && verified_creators.len() <= 5, 45 | SolventError::VerifiedCreatorsInvalid 46 | ) 47 | } 48 | CollectionInfo::V2 { collection_mint: _ } => {} 49 | }; 50 | 51 | Ok(()) 52 | } 53 | 54 | // Verify in the NFT belongs to the collection 55 | pub fn verify_collection( 56 | metadata: &AccountInfo, 57 | collection_info: &CollectionInfo, 58 | whitelist_proof: Option>, 59 | ) -> bool { 60 | let metadata = Metadata::from_account_info(metadata).unwrap(); 61 | 62 | match collection_info { 63 | CollectionInfo::V1 { 64 | symbol, 65 | verified_creators, 66 | whitelist_root, 67 | } => { 68 | // Check if the symbol matches 69 | let trimmed_symbol = metadata.data.symbol.trim_matches(char::from(0)); 70 | let valid_symbol = trimmed_symbol == symbol; 71 | 72 | // Check if at least one NFT creator exists in BucketState's verified creators 73 | let creators = metadata.data.creators.unwrap(); 74 | let mut valid_creator = false; 75 | if !verified_creators.is_empty() { 76 | valid_creator = creators.iter().any(|creator| { 77 | creator.verified 78 | && verified_creators.iter().any(|additional_verified_creator| { 79 | creator.address == *additional_verified_creator 80 | }) 81 | }); 82 | } 83 | 84 | // Check if NFT exists in whitelist 85 | let leaf = anchor_lang::solana_program::keccak::hash(&metadata.mint.to_bytes()).0; 86 | let in_whitelist = verify_proof(whitelist_proof.unwrap(), *whitelist_root, leaf); 87 | 88 | valid_symbol && valid_creator && in_whitelist 89 | } 90 | 91 | CollectionInfo::V2 { collection_mint } => match metadata.collection { 92 | // Check that the collection field exists 93 | None => false, 94 | Some(collection) => { 95 | // Check that the collection mint matches, and verified is true 96 | collection.key == *collection_mint && collection.verified 97 | } 98 | }, 99 | } 100 | } 101 | 102 | // Check if NFT's royalties are more than 90%, implying the NFT is stolen and thus banned 103 | pub fn is_nft_banned(metadata: &AccountInfo) -> bool { 104 | let metadata = Metadata::from_account_info(metadata).unwrap(); 105 | metadata.data.seller_fee_basis_points >= SELLER_FEE_BASIS_POINTS_THRESHOLD_FOR_BAN 106 | } 107 | 108 | #[derive(PartialEq, Eq, Debug)] 109 | pub struct CalculateLoanArgs { 110 | pub max_locker_duration: u64, 111 | pub num_nfts_in_bucket: u16, 112 | pub num_nfts_in_lockers: u16, 113 | pub interest_scaler: u8, 114 | pub locker_duration: u64, 115 | } 116 | 117 | #[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Debug)] 118 | pub struct CalculateLoanResult { 119 | pub principal_amount: u64, 120 | pub max_interest_payable: u64, 121 | } 122 | 123 | // Returns tuple of pricipal amount and max interest payable 124 | pub fn calculate_loan(args: CalculateLoanArgs) -> CalculateLoanResult { 125 | let x = args.num_nfts_in_bucket; 126 | let y = args.num_nfts_in_lockers; 127 | 128 | let x_plus_y = (y as u64).checked_add(x as u64).unwrap(); 129 | 130 | let numerator = args 131 | .locker_duration 132 | .checked_mul(y as u64) 133 | .unwrap() 134 | .checked_mul(DROPLETS_PER_NFT as u64) 135 | .unwrap() 136 | .checked_mul(LAMPORTS_PER_DROPLET) 137 | .unwrap(); 138 | 139 | let denominator = args.max_locker_duration.checked_mul(x_plus_y).unwrap(); 140 | 141 | let raw_interest = numerator 142 | .checked_add(denominator.checked_sub(1).unwrap()) 143 | .unwrap() 144 | .checked_div(denominator) 145 | .unwrap(); 146 | 147 | let scaled_interest = raw_interest 148 | .checked_mul(args.interest_scaler as u64) 149 | .unwrap() 150 | .checked_div(MAX_INTEREST_SCALER as u64) 151 | .unwrap(); 152 | 153 | let max_droplets_per_nft = LAMPORTS_PER_DROPLET 154 | .checked_mul(DROPLETS_PER_NFT as u64) 155 | .unwrap(); 156 | 157 | let principal_amount = max_droplets_per_nft.checked_sub(raw_interest).unwrap(); 158 | 159 | CalculateLoanResult { 160 | principal_amount, 161 | max_interest_payable: scaled_interest, 162 | } 163 | } 164 | 165 | pub fn verify_proof(proof: Vec<[u8; 32]>, root: [u8; 32], leaf: [u8; 32]) -> bool { 166 | let mut computed_hash = leaf; 167 | for proof_element in proof.into_iter() { 168 | if computed_hash <= proof_element { 169 | // Hash(current computed hash + current element of the proof) 170 | computed_hash = 171 | anchor_lang::solana_program::keccak::hashv(&[&computed_hash, &proof_element]).0; 172 | } else { 173 | // Hash(current element of the proof + current computed hash) 174 | computed_hash = 175 | anchor_lang::solana_program::keccak::hashv(&[&proof_element, &computed_hash]).0; 176 | } 177 | } 178 | // Check if the computed hash (root) is equal to the provided root 179 | computed_hash == root 180 | } 181 | 182 | fn parse_farm(info: &AccountInfo) -> Result { 183 | let mut data: &[u8] = &info.try_borrow_data()?; 184 | Farm::try_deserialize(&mut data) 185 | } 186 | 187 | pub fn parse_farmer(info: &AccountInfo) -> Result { 188 | let mut data: &[u8] = &info.try_borrow_data()?; 189 | Farmer::try_deserialize(&mut data) 190 | } 191 | 192 | //#[soteria(ignore_redundant)] 193 | pub fn validate_bank(farm: &AccountInfo, bank: &AccountInfo) -> Result { 194 | let farm = parse_farm(farm)?; 195 | let result = farm.bank == bank.key(); 196 | Ok(result) 197 | } 198 | 199 | // Assert farm's config is sane and suitable for staking 200 | pub fn validate_farm(info: &AccountInfo) -> Result { 201 | let farm = parse_farm(info)?; 202 | let is_valid = farm.config.min_staking_period_sec == 0 203 | && farm.config.cooldown_period_sec == 0 204 | && (farm.config.unstaking_fee_lamp == 0 || farm.config.unstaking_fee_lamp >= 890880); 205 | Ok(is_valid) 206 | } 207 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/constants.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_lang::solana_program::pubkey; 3 | 4 | // Numbers 5 | pub const LAMPORTS_PER_DROPLET: u64 = 100000000; 6 | pub const MAX_INTEREST_SCALER: u8 = 100; 7 | pub const LIQUIDATION_REWARD_PERCENTAGE: u8 = 20; 8 | pub const DROPLETS_PER_NFT: u8 = 100; 9 | pub const REDEEM_FEE_BASIS_POINTS: u8 = 0; 10 | pub const SWAP_FEE_BASIS_POINTS: u8 = 50; 11 | pub const DISTRIBUTOR_FEE_BASIS_POINTS: u16 = 1000; 12 | pub const SELLER_FEE_BASIS_POINTS_THRESHOLD_FOR_BAN: u16 = 9500; 13 | 14 | // Seed strings 15 | pub const SOLVENT_AUTHORITY_SEED: &str = "authority-seed"; 16 | pub const FARMER_AUTHORITY_SEED: &str = "farmer-authority-seed"; 17 | pub const BUCKET_SEED: &str = "bucket-seed-v3"; 18 | pub const LOCKER_SEED: &str = "locker-seed"; 19 | pub const DEPOSIT_SEED: &str = "deposit-seed"; 20 | pub const SWAP_SEED: &str = "swap-seed"; 21 | pub const GEMWORKS_FARMER_SEED: &str = "farmer"; 22 | pub const GEMWORKS_VAULT_SEED: &str = "vault"; 23 | pub const GEMWORKS_GEM_BOX_SEED: &str = "gem_box"; 24 | pub const GEMWORKS_GDR_SEED: &str = "gem_deposit_receipt"; 25 | pub const GEMWORKS_GEM_RARITY_SEED: &str = "gem_rarity"; 26 | pub const GEMWORKS_FARM_TREASURY_SEED: &str = "treasury"; 27 | pub const GEMWORKS_REWARD_POT_SEED: &str = "reward_pot"; 28 | // TODO: To remove after migration is done 29 | pub const MIGRATION_SEED: &str = "migration-seed"; 30 | 31 | // Pubkeys 32 | pub const SOLVENT_CORE_TREASURY: Pubkey = pubkey!("45nueWN9Qwn5vDBmJGBLEsYvaJG6vrNmNdCyrntXDk2K"); 33 | pub const SOLVENT_LOCKERS_TREASURY: Pubkey = 34 | pubkey!("HkjFiwUW7qnREVm2PxBg8LUrCvjExrJjyYY51wsZTUK8"); 35 | 36 | #[cfg(feature = "test-ids")] 37 | pub const SOLVENT_MIGRATION_CRANK: Pubkey = pubkey!("DPnNwkEzRLxeL1k3ftkSYNgbUDaWyi37VQArW56v8xok"); 38 | #[cfg(not(feature = "test-ids"))] 39 | pub const SOLVENT_MIGRATION_CRANK: Pubkey = pubkey!("Hr4eSwCbeaFL1DVVDwPx18DGgnfQmYX6VkbXk66mYnnn"); 40 | 41 | #[cfg(feature = "test-ids")] 42 | pub const SOLVENT_ADMIN: Pubkey = pubkey!("DPnNwkEzRLxeL1k3ftkSYNgbUDaWyi37VQArW56v8xok"); 43 | #[cfg(not(feature = "test-ids"))] 44 | pub const SOLVENT_ADMIN: Pubkey = pubkey!("DYJXfxaci8NzfkHRZ87Ycfwp1CMMwssXcKeN8hWTbons"); 45 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/errors.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum SolventError { 5 | #[msg("The NFT collection symbol you entered is invalid.")] 6 | CollectionSymbolInvalid, 7 | 8 | #[msg("There should be 1 to 5 verified creators.")] 9 | VerifiedCreatorsInvalid, 10 | 11 | #[msg("Failed to verify if the NFT belongs to the collection.")] 12 | CollectionVerificationFailed, 13 | 14 | #[msg("Locking duration entered by you is too long.")] 15 | LockerDurationInvalid, 16 | 17 | #[msg("There are no NFTs in the bucket.")] 18 | BucketEmpty, 19 | 20 | #[msg("The locker has expired and it's up for liquidation.")] 21 | LockerExpired, 22 | 23 | #[msg("The locker is still active and not up for liquidation.")] 24 | LockerActive, 25 | 26 | #[msg("You have insufficient droplets to unlock NFT.")] 27 | DropletsInsufficient, 28 | 29 | #[msg("Interest scaler entered by you is larger than the max value.")] 30 | InterestScalerInvalid, 31 | 32 | #[msg("Lockers feature is disabled on this collection.")] 33 | LockersDisabled, 34 | 35 | #[msg("You do not have administrator access.")] 36 | AdminAccessUnauthorized, 37 | 38 | #[msg("The Solvent treasury account entered by you is invalid.")] 39 | SolventTreasuryInvalid, 40 | 41 | #[msg("The locker is not owned by you.")] 42 | LockerAccessUnauthorized, 43 | 44 | #[msg("If farming is enabled, farming params must have all 5 Pubkeys.")] 45 | FarmingParamsInvalid, 46 | 47 | #[msg("The farm is not suitable for staking.")] 48 | FarmConfigInvalid, 49 | 50 | #[msg("Auto-staking feature is disabled on this collection.")] 51 | StakingDisabled, 52 | 53 | #[msg("The staked NFT is in cooldown period, please wait and try again.")] 54 | StakingCooldownPending, 55 | 56 | #[msg("You cannot redeem NFT as part of a swap because you haven't deposited one yet.")] 57 | SwapNotAllowed, 58 | 59 | #[msg("There was an internal error while calculating fee distribution.")] 60 | FeeDistributionIncorrect, 61 | 62 | #[msg("The Solvent migration crank account entered by you is invalid.")] 63 | SolventMigrationCrankInvalid, 64 | 65 | #[msg("Cannot deposit additional NFT before completing the pending swap.")] 66 | SwapPending, 67 | 68 | #[msg("NFT is banned from Solvent because it's likely stolen.")] 69 | NftBanned, 70 | } 71 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/claim_balance.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::*; 2 | use crate::errors::*; 3 | use anchor_lang::prelude::*; 4 | use anchor_spl::token::Token; 5 | 6 | // Claim the SOL balance of solvent-authority 7 | pub fn claim_balance(ctx: Context) -> Result<()> { 8 | // Get Solvent authority signer seeds 9 | let solvent_authority_bump = *ctx.bumps.get("solvent_authority").unwrap(); 10 | let solvent_authority_seeds = &[SOLVENT_AUTHORITY_SEED.as_bytes(), &[solvent_authority_bump]]; 11 | let solvent_authority_signer_seeds = &[&solvent_authority_seeds[..]]; 12 | 13 | let amount_to_claim = (ctx.accounts.solvent_authority.lamports() as u64) 14 | .checked_sub(Rent::minimum_balance(&ctx.accounts.rent, 0_usize)) 15 | .unwrap(); 16 | 17 | // Transfer lamports from Solvent authority to Solvent treasury 18 | let tranfer_lamports_ix = anchor_lang::solana_program::system_instruction::transfer( 19 | &ctx.accounts.solvent_authority.key(), 20 | &ctx.accounts.solvent_treasury.key(), 21 | amount_to_claim, 22 | ); 23 | 24 | anchor_lang::solana_program::program::invoke_signed( 25 | &tranfer_lamports_ix, 26 | &[ 27 | ctx.accounts.solvent_authority.to_account_info(), 28 | ctx.accounts.solvent_treasury.to_account_info(), 29 | ], 30 | solvent_authority_signer_seeds, 31 | )?; 32 | 33 | // Emit success event 34 | emit!(ClaimBalanceEvent { 35 | signer: ctx.accounts.signer.key(), 36 | sol_amount_claimed: amount_to_claim 37 | }); 38 | 39 | Ok(()) 40 | } 41 | 42 | #[derive(Accounts)] 43 | pub struct ClaimBalance<'info> { 44 | pub signer: Signer<'info>, 45 | 46 | #[account( 47 | mut, 48 | address = SOLVENT_CORE_TREASURY @ SolventError::SolventTreasuryInvalid 49 | )] 50 | /// CHECK: Safe because there are enough constraints set 51 | pub solvent_treasury: UncheckedAccount<'info>, 52 | 53 | #[account( 54 | mut, 55 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 56 | bump, 57 | )] 58 | /// CHECK: Safe because this read-only account only gets used as a constraint 59 | pub solvent_authority: UncheckedAccount<'info>, 60 | 61 | // Solana ecosystem program addresses 62 | pub token_program: Program<'info, Token>, 63 | pub system_program: Program<'info, System>, 64 | pub rent: Sysvar<'info, Rent>, 65 | } 66 | 67 | #[event] 68 | pub struct ClaimBalanceEvent { 69 | pub signer: Pubkey, 70 | pub sol_amount_claimed: u64, 71 | } 72 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/create_bucket.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use crate::constants::*; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::token::{Mint, Token}; 6 | 7 | // Create a bucket for an NFT collection 8 | pub fn create_bucket(ctx: Context, collection_info: CollectionInfo) -> Result<()> { 9 | // Make sure collection info is valid 10 | validate_collection_info(&collection_info)?; 11 | 12 | // Update BucketState account with information 13 | **ctx.accounts.bucket_state = BucketStateV3 { 14 | bump: *ctx.bumps.get("bucket_state").unwrap(), 15 | droplet_mint: ctx.accounts.droplet_mint.key(), 16 | collection_info: collection_info.clone(), 17 | // Counters 18 | num_nfts_in_bucket: 0, 19 | num_nfts_in_lockers: 0, 20 | // Locker related params 21 | is_locking_enabled: false, 22 | max_locker_duration: 0, 23 | interest_scaler: 0, 24 | // NFT staking related params 25 | is_staking_enabled: false, 26 | staking_params: None, 27 | }; 28 | 29 | // Emit success event 30 | emit!(CreateBucketEvent { 31 | droplet_mint: ctx.accounts.droplet_mint.key(), 32 | signer: ctx.accounts.signer.key(), 33 | collection_info 34 | }); 35 | 36 | Ok(()) 37 | } 38 | 39 | #[derive(Accounts)] 40 | pub struct CreateBucket<'info> { 41 | #[account(mut)] 42 | pub signer: Signer<'info>, 43 | 44 | #[account( 45 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 46 | bump, 47 | )] 48 | /// CHECK: Safe because this read-only account only gets used as a constraint 49 | pub solvent_authority: UncheckedAccount<'info>, 50 | 51 | #[account( 52 | init, 53 | seeds = [ 54 | droplet_mint.key.as_ref(), 55 | BUCKET_SEED.as_bytes() 56 | ], 57 | payer = signer, 58 | space = BucketStateV3::LEN, 59 | bump 60 | )] 61 | pub bucket_state: Box>, 62 | 63 | #[account( 64 | init, 65 | mint::decimals = 8, 66 | mint::authority = solvent_authority, 67 | payer = signer 68 | )] 69 | pub droplet_mint: Account<'info, Mint>, 70 | 71 | // Solana ecosystem program addresses 72 | pub token_program: Program<'info, Token>, 73 | pub system_program: Program<'info, System>, 74 | pub rent: Sysvar<'info, Rent>, 75 | } 76 | 77 | #[event] 78 | pub struct CreateBucketEvent { 79 | pub signer: Pubkey, 80 | pub droplet_mint: Pubkey, 81 | pub collection_info: CollectionInfo, 82 | } 83 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/deposit_nft.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use crate::constants::*; 3 | use crate::errors::*; 4 | use crate::state::*; 5 | use anchor_lang::prelude::*; 6 | use anchor_spl::associated_token::AssociatedToken; 7 | use anchor_spl::token; 8 | use anchor_spl::token::{Mint, Token, TokenAccount}; 9 | 10 | // Deposit an NFT into a bucket and get droplets in exchange 11 | pub fn deposit_nft( 12 | ctx: Context, 13 | swap: bool, 14 | _whitelist_proof: Option>, 15 | ) -> Result<()> { 16 | // Prevent deposits when there's a pending swap 17 | if swap && ctx.accounts.swap_state.flag { 18 | return err!(SolventError::SwapPending); 19 | } 20 | 21 | // Set DepositState account contents 22 | *ctx.accounts.deposit_state = DepositState { 23 | bump: *ctx.bumps.get("deposit_state").unwrap(), 24 | droplet_mint: ctx.accounts.droplet_mint.key(), 25 | nft_mint: ctx.accounts.nft_mint.key(), 26 | }; 27 | 28 | // Set SwapState account contents 29 | *ctx.accounts.swap_state = SwapState { 30 | bump: *ctx.bumps.get("swap_state").unwrap(), 31 | signer: ctx.accounts.signer.key(), 32 | droplet_mint: ctx.accounts.droplet_mint.key(), 33 | flag: swap, 34 | }; 35 | 36 | // Transfer NFT to bucket's token account 37 | let transfer_nft_ctx = CpiContext::new( 38 | ctx.accounts.token_program.to_account_info().clone(), 39 | token::Transfer { 40 | from: ctx 41 | .accounts 42 | .signer_nft_token_account 43 | .to_account_info() 44 | .clone(), 45 | to: ctx 46 | .accounts 47 | .solvent_nft_token_account 48 | .to_account_info() 49 | .clone(), 50 | authority: ctx.accounts.signer.to_account_info().clone(), 51 | }, 52 | ); 53 | token::transfer(transfer_nft_ctx, 1)?; 54 | 55 | // Get Solvent authority signer seeds 56 | let solvent_authority_bump = *ctx.bumps.get("solvent_authority").unwrap(); 57 | let solvent_authority_seeds = &[SOLVENT_AUTHORITY_SEED.as_bytes(), &[solvent_authority_bump]]; 58 | let solvent_authority_signer_seeds = &[&solvent_authority_seeds[..]]; 59 | 60 | if !swap { 61 | // Mint droplets to destination account 62 | let mint_droplets_ctx = CpiContext::new_with_signer( 63 | ctx.accounts.token_program.to_account_info().clone(), 64 | token::MintTo { 65 | mint: ctx.accounts.droplet_mint.to_account_info().clone(), 66 | to: ctx 67 | .accounts 68 | .destination_droplet_token_account 69 | .to_account_info() 70 | .clone(), 71 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 72 | }, 73 | solvent_authority_signer_seeds, 74 | ); 75 | token::mint_to( 76 | mint_droplets_ctx, 77 | DROPLETS_PER_NFT as u64 * LAMPORTS_PER_DROPLET, 78 | )?; 79 | } 80 | 81 | // Increment counter 82 | ctx.accounts.bucket_state.num_nfts_in_bucket = ctx 83 | .accounts 84 | .bucket_state 85 | .num_nfts_in_bucket 86 | .checked_add(1) 87 | .unwrap(); 88 | 89 | // Emit success event 90 | emit!(DepositNftEvent { 91 | droplet_mint: ctx.accounts.droplet_mint.key(), 92 | nft_mint: ctx.accounts.nft_mint.key(), 93 | signer: ctx.accounts.signer.key(), 94 | signer_nft_token_account: ctx.accounts.signer_nft_token_account.key(), 95 | destination_droplet_token_account: ctx.accounts.destination_droplet_token_account.key(), 96 | swap, 97 | // Counters 98 | num_nfts_in_bucket: ctx.accounts.bucket_state.num_nfts_in_bucket, 99 | }); 100 | 101 | Ok(()) 102 | } 103 | 104 | #[derive(Accounts)] 105 | #[instruction(_swap: bool, whitelist_proof: Option>)] 106 | pub struct DepositNft<'info> { 107 | #[account(mut)] 108 | pub signer: Signer<'info>, 109 | 110 | #[account( 111 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 112 | bump, 113 | )] 114 | /// CHECK: Safe because this read-only account only gets used as a constraint 115 | pub solvent_authority: UncheckedAccount<'info>, 116 | 117 | #[account( 118 | mut, 119 | seeds = [ 120 | droplet_mint.key().as_ref(), 121 | BUCKET_SEED.as_bytes() 122 | ], 123 | bump = bucket_state.bump, 124 | has_one = droplet_mint 125 | )] 126 | pub bucket_state: Box>, 127 | 128 | #[account( 129 | init, 130 | seeds = [ 131 | droplet_mint.key().as_ref(), 132 | nft_mint.key().as_ref(), 133 | DEPOSIT_SEED.as_bytes() 134 | ], 135 | bump, 136 | payer = signer, 137 | space = DepositState::LEN 138 | )] 139 | pub deposit_state: Account<'info, DepositState>, 140 | 141 | #[account( 142 | init_if_needed, 143 | seeds = [ 144 | droplet_mint.key().as_ref(), 145 | signer.key().as_ref(), 146 | SWAP_SEED.as_bytes() 147 | ], 148 | bump, 149 | payer = signer, 150 | space = SwapState::LEN 151 | )] 152 | pub swap_state: Account<'info, SwapState>, 153 | 154 | #[account(mut)] 155 | pub droplet_mint: Account<'info, Mint>, 156 | 157 | pub nft_mint: Account<'info, Mint>, 158 | 159 | #[account( 160 | address = mpl_token_metadata::pda::find_metadata_account(&nft_mint.key()).0, 161 | constraint = mpl_token_metadata::check_id(nft_metadata.owner), 162 | constraint = verify_collection(&nft_metadata, &bucket_state.collection_info, whitelist_proof) @ SolventError::CollectionVerificationFailed, 163 | constraint = !is_nft_banned(&nft_metadata) @ SolventError::NftBanned 164 | )] 165 | /// CHECK: Safe because there are already enough constraints 166 | pub nft_metadata: UncheckedAccount<'info>, 167 | 168 | #[account( 169 | mut, 170 | constraint = signer_nft_token_account.mint == nft_mint.key() 171 | )] 172 | pub signer_nft_token_account: Box>, 173 | 174 | #[account( 175 | init_if_needed, 176 | payer = signer, 177 | associated_token::mint = nft_mint, 178 | associated_token::authority = solvent_authority, 179 | )] 180 | pub solvent_nft_token_account: Box>, 181 | 182 | #[account( 183 | mut, 184 | constraint = destination_droplet_token_account.mint == droplet_mint.key() 185 | )] 186 | pub destination_droplet_token_account: Box>, 187 | 188 | // Solana ecosystem program addresses 189 | pub token_program: Program<'info, Token>, 190 | pub associated_token_program: Program<'info, AssociatedToken>, 191 | pub system_program: Program<'info, System>, 192 | pub rent: Sysvar<'info, Rent>, 193 | } 194 | 195 | #[event] 196 | pub struct DepositNftEvent { 197 | pub signer: Pubkey, 198 | pub droplet_mint: Pubkey, 199 | pub nft_mint: Pubkey, 200 | pub signer_nft_token_account: Pubkey, 201 | pub destination_droplet_token_account: Pubkey, 202 | pub swap: bool, 203 | // Counters 204 | pub num_nfts_in_bucket: u16, 205 | } 206 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/liquidate_locker.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::*; 2 | use crate::errors::*; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::associated_token::{get_associated_token_address, AssociatedToken}; 6 | use anchor_spl::token; 7 | use anchor_spl::token::{Mint, Token, TokenAccount}; 8 | 9 | // Liquidate a locked NFT position that is defaulted 10 | pub fn liquidate_locker(ctx: Context) -> Result<()> { 11 | let clock: Clock = Clock::get().unwrap(); 12 | 13 | // Verify that the loan has defaulted and is up for liquidation 14 | require!( 15 | clock.unix_timestamp as u64 16 | > ctx 17 | .accounts 18 | .locker_state 19 | .creation_timestamp 20 | .checked_add(ctx.accounts.locker_state.duration) 21 | .unwrap(), 22 | SolventError::LockerActive 23 | ); 24 | 25 | // Calculate additional droplets to mint 26 | let droplets_to_mint = (DROPLETS_PER_NFT as u64) 27 | .checked_mul(LAMPORTS_PER_DROPLET) 28 | .unwrap() 29 | .checked_sub(ctx.accounts.locker_state.principal_amount) 30 | .unwrap(); 31 | 32 | // Calculate the split between Solvent treasury and liquidation reward 33 | let liquidation_reward = droplets_to_mint 34 | .checked_mul(LIQUIDATION_REWARD_PERCENTAGE as u64) 35 | .unwrap() 36 | .checked_div(DROPLETS_PER_NFT as u64) 37 | .unwrap(); 38 | let to_store_in_vault = droplets_to_mint.checked_sub(liquidation_reward).unwrap(); 39 | 40 | // Get Solvent authority signer seeds 41 | let solvent_authority_bump = *ctx.bumps.get("solvent_authority").unwrap(); 42 | let solvent_authority_seeds = &[SOLVENT_AUTHORITY_SEED.as_bytes(), &[solvent_authority_bump]]; 43 | let solvent_authority_signer_seeds = &[&solvent_authority_seeds[..]]; 44 | 45 | // Mint additional droplets and send to Solvent treasury 46 | let mint_droplets_to_vault_ctx = CpiContext::new_with_signer( 47 | ctx.accounts.token_program.to_account_info().clone(), 48 | token::MintTo { 49 | mint: ctx.accounts.droplet_mint.to_account_info().clone(), 50 | to: ctx 51 | .accounts 52 | .solvent_treasury_droplet_token_account 53 | .to_account_info() 54 | .clone(), 55 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 56 | }, 57 | solvent_authority_signer_seeds, 58 | ); 59 | token::mint_to(mint_droplets_to_vault_ctx, to_store_in_vault)?; 60 | 61 | // Mint liquidation reward droplets and send to signer 62 | let mint_droplets_to_signer_ctx = CpiContext::new_with_signer( 63 | ctx.accounts.token_program.to_account_info().clone(), 64 | token::MintTo { 65 | mint: ctx.accounts.droplet_mint.to_account_info().clone(), 66 | to: ctx 67 | .accounts 68 | .signer_droplet_token_account 69 | .to_account_info() 70 | .clone(), 71 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 72 | }, 73 | solvent_authority_signer_seeds, 74 | ); 75 | token::mint_to(mint_droplets_to_signer_ctx, liquidation_reward)?; 76 | 77 | // Set DepositState account contents 78 | *ctx.accounts.deposit_state = DepositState { 79 | bump: *ctx.bumps.get("deposit_state").unwrap(), 80 | droplet_mint: ctx.accounts.droplet_mint.key(), 81 | nft_mint: ctx.accounts.nft_mint.key(), 82 | }; 83 | 84 | // Update counters 85 | ctx.accounts.bucket_state.num_nfts_in_lockers = ctx 86 | .accounts 87 | .bucket_state 88 | .num_nfts_in_lockers 89 | .checked_sub(1) 90 | .unwrap(); 91 | ctx.accounts.bucket_state.num_nfts_in_bucket = ctx 92 | .accounts 93 | .bucket_state 94 | .num_nfts_in_bucket 95 | .checked_add(1) 96 | .unwrap(); 97 | 98 | // Emit success event 99 | emit!(LiquidateLockerEvent { 100 | droplet_mint: ctx.accounts.droplet_mint.key(), 101 | nft_mint: ctx.accounts.nft_mint.key(), 102 | signer: ctx.accounts.signer.key(), 103 | signer_droplet_token_account: ctx.accounts.signer_droplet_token_account.key(), 104 | // LockerState params 105 | creation_timestamp: ctx.accounts.locker_state.creation_timestamp, 106 | duration: ctx.accounts.locker_state.duration, 107 | principal_amount: ctx.accounts.locker_state.principal_amount, 108 | max_interest_payable: ctx.accounts.locker_state.max_interest_payable, 109 | // Counters 110 | num_nfts_in_bucket: ctx.accounts.bucket_state.num_nfts_in_bucket, 111 | num_nfts_in_lockers: ctx.accounts.bucket_state.num_nfts_in_lockers, 112 | }); 113 | 114 | Ok(()) 115 | } 116 | 117 | #[derive(Accounts)] 118 | pub struct LiquidateLocker<'info> { 119 | #[account(mut)] 120 | pub signer: Signer<'info>, 121 | 122 | #[account( 123 | mut, 124 | seeds = [ 125 | droplet_mint.key().as_ref(), 126 | BUCKET_SEED.as_bytes() 127 | ], 128 | bump = bucket_state.bump, 129 | constraint = bucket_state.is_locking_enabled @ SolventError::LockersDisabled, 130 | has_one = droplet_mint 131 | )] 132 | pub bucket_state: Box>, 133 | 134 | pub nft_mint: Account<'info, Mint>, 135 | 136 | #[account( 137 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 138 | bump, 139 | )] 140 | /// CHECK: Safe because this read-only account only gets used as a constraint 141 | pub solvent_authority: UncheckedAccount<'info>, 142 | 143 | #[account( 144 | mut, 145 | seeds = [ 146 | droplet_mint.key().as_ref(), 147 | nft_mint.key().as_ref(), 148 | LOCKER_SEED.as_bytes() 149 | ], 150 | bump = locker_state.bump, 151 | has_one = droplet_mint, 152 | has_one = nft_mint, 153 | close = signer 154 | )] 155 | pub locker_state: Account<'info, LockerState>, 156 | 157 | #[account( 158 | init, 159 | seeds = [ 160 | droplet_mint.key().as_ref(), 161 | nft_mint.key().as_ref(), 162 | DEPOSIT_SEED.as_bytes() 163 | ], 164 | bump, 165 | payer = signer, 166 | space = DepositState::LEN 167 | )] 168 | pub deposit_state: Account<'info, DepositState>, 169 | 170 | #[account(mut)] 171 | pub droplet_mint: Account<'info, Mint>, 172 | 173 | #[account(address = SOLVENT_LOCKERS_TREASURY @ SolventError::SolventTreasuryInvalid)] 174 | /// CHECK: Safe because this read-only account only gets used as a constraint 175 | pub solvent_treasury: UncheckedAccount<'info>, 176 | 177 | #[account( 178 | init_if_needed, 179 | payer = signer, 180 | associated_token::mint = droplet_mint, 181 | associated_token::authority = solvent_treasury, 182 | )] 183 | pub solvent_treasury_droplet_token_account: Box>, 184 | 185 | #[account( 186 | mut, 187 | constraint = signer_droplet_token_account.mint == droplet_mint.key() 188 | )] 189 | pub signer_droplet_token_account: Box>, 190 | 191 | #[account( 192 | address = get_associated_token_address(solvent_authority.key, &nft_mint.key()), 193 | )] 194 | pub solvent_nft_token_account: Box>, 195 | 196 | // Solana ecosystem program addresses 197 | pub system_program: Program<'info, System>, 198 | pub token_program: Program<'info, Token>, 199 | pub associated_token_program: Program<'info, AssociatedToken>, 200 | pub rent: Sysvar<'info, Rent>, 201 | } 202 | 203 | #[event] 204 | pub struct LiquidateLockerEvent { 205 | pub signer: Pubkey, 206 | pub nft_mint: Pubkey, 207 | pub droplet_mint: Pubkey, 208 | pub signer_droplet_token_account: Pubkey, 209 | // LockerState params 210 | pub creation_timestamp: u64, 211 | pub duration: u64, 212 | pub principal_amount: u64, 213 | pub max_interest_payable: u64, 214 | // Counters 215 | pub num_nfts_in_bucket: u16, 216 | pub num_nfts_in_lockers: u16, 217 | } 218 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/lock_nft.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use crate::constants::*; 3 | use crate::errors::*; 4 | use crate::state::*; 5 | use anchor_lang::prelude::*; 6 | use anchor_spl::associated_token::AssociatedToken; 7 | use anchor_spl::token; 8 | use anchor_spl::token::{Mint, Token, TokenAccount}; 9 | 10 | // Lock an NFT into a locker and get droplets in exchange 11 | pub fn lock_nft( 12 | ctx: Context, 13 | duration: u64, 14 | _whitelist_proof: Option>, 15 | ) -> Result { 16 | // Ensure there's at least 1 NFT in bucket 17 | require!( 18 | ctx.accounts.bucket_state.num_nfts_in_bucket > 0, 19 | SolventError::BucketEmpty 20 | ); 21 | 22 | // Ensure locking duration is not too long 23 | require!( 24 | duration <= ctx.accounts.bucket_state.max_locker_duration, 25 | SolventError::LockerDurationInvalid 26 | ); 27 | 28 | // Increment counter 29 | ctx.accounts.bucket_state.num_nfts_in_lockers = ctx 30 | .accounts 31 | .bucket_state 32 | .num_nfts_in_lockers 33 | .checked_add(1) 34 | .unwrap(); 35 | 36 | // Calculate loan amount and interest rate 37 | let calculate_loan_result = calculate_loan(CalculateLoanArgs { 38 | max_locker_duration: ctx.accounts.bucket_state.max_locker_duration, 39 | num_nfts_in_bucket: ctx.accounts.bucket_state.num_nfts_in_bucket, 40 | num_nfts_in_lockers: ctx.accounts.bucket_state.num_nfts_in_lockers, 41 | interest_scaler: ctx.accounts.bucket_state.interest_scaler, 42 | locker_duration: duration, 43 | }); 44 | 45 | // Transfer NFT to locker token account 46 | let nft_transfer_ctx = CpiContext::new( 47 | ctx.accounts.token_program.to_account_info().clone(), 48 | token::Transfer { 49 | from: ctx 50 | .accounts 51 | .signer_nft_token_account 52 | .to_account_info() 53 | .clone(), 54 | to: ctx 55 | .accounts 56 | .solvent_nft_token_account 57 | .to_account_info() 58 | .clone(), 59 | authority: ctx.accounts.signer.to_account_info().clone(), 60 | }, 61 | ); 62 | token::transfer(nft_transfer_ctx, 1)?; 63 | 64 | // Get Solvent authority signer seeds 65 | let solvent_authority_bump = *ctx.bumps.get("solvent_authority").unwrap(); 66 | let solvent_authority_seeds = &[SOLVENT_AUTHORITY_SEED.as_bytes(), &[solvent_authority_bump]]; 67 | let solvent_authority_signer_seeds = &[&solvent_authority_seeds[..]]; 68 | 69 | // Mint droplets to destination account 70 | let mint_droplets_ctx = CpiContext::new_with_signer( 71 | ctx.accounts.token_program.to_account_info().clone(), 72 | token::MintTo { 73 | mint: ctx.accounts.droplet_mint.to_account_info().clone(), 74 | to: ctx 75 | .accounts 76 | .destination_droplet_token_account 77 | .to_account_info() 78 | .clone(), 79 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 80 | }, 81 | solvent_authority_signer_seeds, 82 | ); 83 | token::mint_to( 84 | mint_droplets_ctx, 85 | calculate_loan_result.principal_amount as u64, 86 | )?; 87 | 88 | // Update LockerState account with information 89 | let clock: Clock = Clock::get().unwrap(); 90 | let creation_timestamp = clock.unix_timestamp as u64; 91 | *ctx.accounts.locker_state = LockerState { 92 | bump: *ctx.bumps.get("locker_state").unwrap(), 93 | droplet_mint: ctx.accounts.droplet_mint.key(), 94 | depositor: ctx.accounts.signer.key(), 95 | nft_mint: ctx.accounts.nft_mint.key(), 96 | duration, 97 | creation_timestamp, 98 | principal_amount: calculate_loan_result.principal_amount, 99 | max_interest_payable: calculate_loan_result.max_interest_payable, 100 | }; 101 | 102 | // Emit success event 103 | emit!(LockNftEvent { 104 | droplet_mint: ctx.accounts.droplet_mint.key(), 105 | nft_mint: ctx.accounts.nft_mint.key(), 106 | signer: ctx.accounts.signer.key(), 107 | signer_nft_token_account: ctx.accounts.signer_nft_token_account.key(), 108 | destination_droplet_token_account: ctx.accounts.destination_droplet_token_account.key(), 109 | // LockerState params 110 | creation_timestamp, 111 | duration, 112 | principal_amount: calculate_loan_result.principal_amount, 113 | max_interest_payable: calculate_loan_result.max_interest_payable, 114 | // BucketState params 115 | interest_scaler: ctx.accounts.bucket_state.interest_scaler, 116 | max_locker_duration: ctx.accounts.bucket_state.max_locker_duration, 117 | num_nfts_in_bucket: ctx.accounts.bucket_state.num_nfts_in_bucket, 118 | num_nfts_in_lockers: ctx.accounts.bucket_state.num_nfts_in_lockers 119 | }); 120 | 121 | Ok(calculate_loan_result) 122 | } 123 | 124 | #[derive(Accounts)] 125 | #[instruction(_duration: u64, whitelist_proof: Option>)] 126 | pub struct LockNft<'info> { 127 | #[account(mut)] 128 | pub signer: Signer<'info>, 129 | 130 | #[account( 131 | mut, 132 | seeds = [ 133 | droplet_mint.key().as_ref(), 134 | BUCKET_SEED.as_bytes() 135 | ], 136 | bump = bucket_state.bump, 137 | constraint = bucket_state.is_locking_enabled @ SolventError::LockersDisabled, 138 | has_one = droplet_mint 139 | )] 140 | pub bucket_state: Box>, 141 | 142 | #[account( 143 | init, 144 | seeds = [ 145 | droplet_mint.key().as_ref(), 146 | nft_mint.key().as_ref(), 147 | LOCKER_SEED.as_bytes() 148 | ], 149 | payer=signer, 150 | space=LockerState::LEN, 151 | bump 152 | )] 153 | pub locker_state: Account<'info, LockerState>, 154 | 155 | pub nft_mint: Account<'info, Mint>, 156 | 157 | #[account( 158 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 159 | bump, 160 | )] 161 | /// CHECK: Safe because this read-only account only gets used as a constraint 162 | pub solvent_authority: UncheckedAccount<'info>, 163 | 164 | #[account( 165 | address = mpl_token_metadata::pda::find_metadata_account(&nft_mint.key()).0, 166 | constraint = mpl_token_metadata::check_id(nft_metadata.owner), 167 | constraint = verify_collection(&nft_metadata, &bucket_state.collection_info, whitelist_proof) @ SolventError::CollectionVerificationFailed, 168 | constraint = !is_nft_banned(&nft_metadata) @ SolventError::NftBanned 169 | )] 170 | /// CHECK: Safe because there are already enough constraints 171 | pub nft_metadata: UncheckedAccount<'info>, 172 | 173 | #[account( 174 | mut, 175 | constraint = signer_nft_token_account.mint == nft_mint.key() 176 | )] 177 | pub signer_nft_token_account: Box>, 178 | 179 | #[account( 180 | init_if_needed, 181 | payer = signer, 182 | associated_token::mint = nft_mint, 183 | associated_token::authority = solvent_authority, 184 | )] 185 | pub solvent_nft_token_account: Box>, 186 | 187 | #[account(mut)] 188 | pub droplet_mint: Account<'info, Mint>, 189 | 190 | #[account( 191 | mut, 192 | constraint = destination_droplet_token_account.mint == droplet_mint.key() 193 | )] 194 | pub destination_droplet_token_account: Box>, 195 | 196 | // Solana ecosystem program addresses 197 | pub system_program: Program<'info, System>, 198 | pub token_program: Program<'info, Token>, 199 | pub associated_token_program: Program<'info, AssociatedToken>, 200 | pub rent: Sysvar<'info, Rent>, 201 | } 202 | 203 | #[event] 204 | pub struct LockNftEvent { 205 | pub signer: Pubkey, 206 | pub droplet_mint: Pubkey, 207 | pub nft_mint: Pubkey, 208 | pub signer_nft_token_account: Pubkey, 209 | pub destination_droplet_token_account: Pubkey, 210 | // BucketState params 211 | pub num_nfts_in_bucket: u16, 212 | pub num_nfts_in_lockers: u16, 213 | pub interest_scaler: u8, 214 | pub max_locker_duration: u64, 215 | // LockerState params 216 | pub creation_timestamp: u64, 217 | pub duration: u64, 218 | pub principal_amount: u64, 219 | pub max_interest_payable: u64, 220 | } 221 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/migrate_droplets.rs: -------------------------------------------------------------------------------- 1 | // TODO: To remove after migration is done 2 | 3 | use crate::constants::*; 4 | use crate::errors::*; 5 | use crate::state::*; 6 | use anchor_lang::prelude::*; 7 | use anchor_spl::associated_token::AssociatedToken; 8 | use anchor_spl::token; 9 | use anchor_spl::token::{Mint, Token, TokenAccount}; 10 | 11 | // Take old droplets from user and mint to him new droplets in exchange 12 | pub fn migrate_droplets(ctx: Context) -> Result<()> { 13 | //#[soteria(ignore)] 14 | let droplets_to_migrate = ctx.accounts.signer_droplet_token_account_old.amount; 15 | 16 | // Transfer old droplets from signer's account to the treasury account 17 | let transfer_droplets_ctx = CpiContext::new( 18 | ctx.accounts.token_program.to_account_info().clone(), 19 | token::Transfer { 20 | from: ctx 21 | .accounts 22 | .signer_droplet_token_account_old 23 | .to_account_info() 24 | .clone(), 25 | to: ctx 26 | .accounts 27 | .solvent_migration_crank_droplet_token_account 28 | .to_account_info() 29 | .clone(), 30 | authority: ctx.accounts.signer.to_account_info().clone(), 31 | }, 32 | ); 33 | token::transfer(transfer_droplets_ctx, droplets_to_migrate)?; 34 | 35 | // Get Solvent authority signer seeds 36 | let solvent_authority_bump = *ctx.bumps.get("solvent_authority").unwrap(); 37 | let solvent_authority_seeds = &[SOLVENT_AUTHORITY_SEED.as_bytes(), &[solvent_authority_bump]]; 38 | let solvent_authority_signer_seeds = &[&solvent_authority_seeds[..]]; 39 | 40 | // Mint droplets to destination account 41 | let mint_droplets_ctx = CpiContext::new_with_signer( 42 | ctx.accounts.token_program.to_account_info().clone(), 43 | token::MintTo { 44 | mint: ctx.accounts.droplet_mint_new.to_account_info().clone(), 45 | to: ctx 46 | .accounts 47 | .signer_droplet_token_account_new 48 | .to_account_info() 49 | .clone(), 50 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 51 | }, 52 | solvent_authority_signer_seeds, 53 | ); 54 | token::mint_to(mint_droplets_ctx, droplets_to_migrate)?; 55 | 56 | // Emit success event 57 | emit!(MigrateDropletsEvent { 58 | droplet_mint_old: ctx.accounts.droplet_mint_old.key(), 59 | droplet_mint_new: ctx.accounts.droplet_mint_new.key(), 60 | signer_droplet_token_account_old: ctx.accounts.signer_droplet_token_account_old.key(), 61 | signer_droplet_token_account_new: ctx.accounts.signer_droplet_token_account_new.key(), 62 | signer: ctx.accounts.signer.key(), 63 | droplet_amount_migrated: droplets_to_migrate 64 | }); 65 | 66 | Ok(()) 67 | } 68 | 69 | #[derive(Accounts)] 70 | pub struct MigrateDroplets<'info> { 71 | #[account(mut)] 72 | pub signer: Signer<'info>, 73 | 74 | #[account( 75 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 76 | bump, 77 | )] 78 | /// CHECK: Safe because this read-only account only gets used as a constraint 79 | pub solvent_authority: UncheckedAccount<'info>, 80 | 81 | #[account(address = SOLVENT_MIGRATION_CRANK @ SolventError::SolventMigrationCrankInvalid)] 82 | /// CHECK: Safe because this read-only account only gets used as a constraint 83 | pub solvent_migration_crank: UncheckedAccount<'info>, 84 | 85 | #[account( 86 | seeds = [ 87 | droplet_mint_new.key().as_ref(), 88 | MIGRATION_SEED.as_bytes() 89 | ], 90 | bump, 91 | has_one = droplet_mint_old, 92 | has_one = droplet_mint_new 93 | )] 94 | pub migration_state: Account<'info, MigrationState>, 95 | 96 | pub droplet_mint_old: Account<'info, Mint>, 97 | 98 | #[account( 99 | mut, 100 | constraint = signer_droplet_token_account_old.mint == droplet_mint_old.key() 101 | )] 102 | pub signer_droplet_token_account_old: Box>, 103 | 104 | #[account(mut)] 105 | pub droplet_mint_new: Account<'info, Mint>, 106 | 107 | #[account( 108 | mut, 109 | constraint = signer_droplet_token_account_new.mint == droplet_mint_new.key() 110 | )] 111 | pub signer_droplet_token_account_new: Box>, 112 | 113 | #[account( 114 | init_if_needed, 115 | payer = signer, 116 | associated_token::mint = droplet_mint_old, 117 | associated_token::authority = solvent_migration_crank, 118 | )] 119 | pub solvent_migration_crank_droplet_token_account: Box>, 120 | 121 | // Solana ecosystem program addresses 122 | pub system_program: Program<'info, System>, 123 | pub token_program: Program<'info, Token>, 124 | pub associated_token_program: Program<'info, AssociatedToken>, 125 | pub rent: Sysvar<'info, Rent>, 126 | } 127 | 128 | #[event] 129 | pub struct MigrateDropletsEvent { 130 | pub signer: Pubkey, 131 | pub droplet_mint_old: Pubkey, 132 | pub droplet_mint_new: Pubkey, 133 | pub signer_droplet_token_account_old: Pubkey, 134 | pub signer_droplet_token_account_new: Pubkey, 135 | pub droplet_amount_migrated: u64, 136 | } 137 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/migrate_nft.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use crate::constants::*; 3 | use crate::errors::SolventError; 4 | use crate::state::*; 5 | use anchor_lang::prelude::*; 6 | use anchor_spl::associated_token::AssociatedToken; 7 | use anchor_spl::token; 8 | use anchor_spl::token::{Mint, Token, TokenAccount}; 9 | 10 | // Deposit an NFT into a bucket and get droplets in exchange 11 | pub fn migrate_nft( 12 | ctx: Context, 13 | _whitelist_proof: Option>, 14 | ) -> Result<()> { 15 | // Set DepositState account contents 16 | *ctx.accounts.deposit_state = DepositState { 17 | bump: *ctx.bumps.get("deposit_state").unwrap(), 18 | droplet_mint: ctx.accounts.droplet_mint.key(), 19 | nft_mint: ctx.accounts.nft_mint.key(), 20 | }; 21 | 22 | // Transfer NFT to bucket's token account 23 | let transfer_nft_ctx = CpiContext::new( 24 | ctx.accounts.token_program.to_account_info().clone(), 25 | token::Transfer { 26 | from: ctx 27 | .accounts 28 | .signer_nft_token_account 29 | .to_account_info() 30 | .clone(), 31 | to: ctx 32 | .accounts 33 | .solvent_nft_token_account 34 | .to_account_info() 35 | .clone(), 36 | authority: ctx.accounts.signer.to_account_info().clone(), 37 | }, 38 | ); 39 | token::transfer(transfer_nft_ctx, 1)?; 40 | 41 | // Increment counter 42 | ctx.accounts.bucket_state.num_nfts_in_bucket = ctx 43 | .accounts 44 | .bucket_state 45 | .num_nfts_in_bucket 46 | .checked_add(1) 47 | .unwrap(); 48 | 49 | // Emit success event 50 | emit!(MigrateNftEvent { 51 | droplet_mint: ctx.accounts.droplet_mint.key(), 52 | nft_mint: ctx.accounts.nft_mint.key(), 53 | signer_nft_token_account: ctx.accounts.signer_nft_token_account.key() 54 | }); 55 | 56 | Ok(()) 57 | } 58 | 59 | #[derive(Accounts)] 60 | #[instruction(whitelist_proof: Option>)] 61 | pub struct MigrateNft<'info> { 62 | #[account( 63 | mut, 64 | address = SOLVENT_MIGRATION_CRANK @ SolventError::AdminAccessUnauthorized 65 | )] 66 | pub signer: Signer<'info>, 67 | 68 | #[account( 69 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 70 | bump, 71 | )] 72 | /// CHECK: Safe because this read-only account only gets used as a constraint 73 | pub solvent_authority: UncheckedAccount<'info>, 74 | 75 | #[account( 76 | mut, 77 | seeds = [ 78 | droplet_mint.key().as_ref(), 79 | BUCKET_SEED.as_bytes() 80 | ], 81 | bump = bucket_state.bump, 82 | has_one = droplet_mint 83 | )] 84 | pub bucket_state: Box>, 85 | 86 | #[account( 87 | init, 88 | seeds = [ 89 | droplet_mint.key().as_ref(), 90 | nft_mint.key().as_ref(), 91 | DEPOSIT_SEED.as_bytes() 92 | ], 93 | bump, 94 | payer = signer, 95 | space = DepositState::LEN 96 | )] 97 | pub deposit_state: Account<'info, DepositState>, 98 | 99 | #[account(mut)] 100 | pub droplet_mint: Account<'info, Mint>, 101 | 102 | pub nft_mint: Account<'info, Mint>, 103 | 104 | #[account( 105 | address = mpl_token_metadata::pda::find_metadata_account(&nft_mint.key()).0, 106 | constraint = mpl_token_metadata::check_id(nft_metadata.owner), 107 | constraint = verify_collection(&nft_metadata, &bucket_state.collection_info, whitelist_proof) @ SolventError::CollectionVerificationFailed 108 | )] 109 | /// CHECK: Safe because there are already enough constraints 110 | pub nft_metadata: UncheckedAccount<'info>, 111 | 112 | #[account( 113 | mut, 114 | constraint = signer_nft_token_account.mint == nft_mint.key() 115 | )] 116 | pub signer_nft_token_account: Box>, 117 | 118 | #[account( 119 | init_if_needed, 120 | payer = signer, 121 | associated_token::mint = nft_mint, 122 | associated_token::authority = solvent_authority, 123 | )] 124 | pub solvent_nft_token_account: Box>, 125 | 126 | // Solana ecosystem program addresses 127 | pub token_program: Program<'info, Token>, 128 | pub associated_token_program: Program<'info, AssociatedToken>, 129 | pub system_program: Program<'info, System>, 130 | pub rent: Sysvar<'info, Rent>, 131 | } 132 | 133 | #[event] 134 | pub struct MigrateNftEvent { 135 | pub droplet_mint: Pubkey, 136 | pub nft_mint: Pubkey, 137 | pub signer_nft_token_account: Pubkey, 138 | } 139 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use claim_balance::*; 2 | pub use create_bucket::*; 3 | pub use deposit_nft::*; 4 | pub use liquidate_locker::*; 5 | pub use lock_nft::*; 6 | pub use migrate_droplets::*; 7 | pub use migrate_nft::*; 8 | pub use redeem_nft::*; 9 | pub use set_locking_enabled::*; 10 | pub use set_staking_enabled::*; 11 | pub use stake_nft::*; 12 | pub use start_migration::*; 13 | pub use unlock_nft::*; 14 | pub use unstake_nft::*; 15 | pub use update_collection_info::*; 16 | pub use update_locking_params::*; 17 | pub use update_staking_params::*; 18 | 19 | pub mod claim_balance; 20 | pub mod create_bucket; 21 | pub mod deposit_nft; 22 | pub mod liquidate_locker; 23 | pub mod lock_nft; 24 | pub mod migrate_droplets; 25 | pub mod migrate_nft; 26 | pub mod redeem_nft; 27 | pub mod set_locking_enabled; 28 | pub mod set_staking_enabled; 29 | pub mod stake_nft; 30 | pub mod start_migration; 31 | pub mod unlock_nft; 32 | pub mod unstake_nft; 33 | pub mod update_collection_info; 34 | pub mod update_locking_params; 35 | pub mod update_staking_params; 36 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/redeem_nft.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::*; 2 | use crate::errors::*; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::associated_token::get_associated_token_address; 6 | use anchor_spl::associated_token::AssociatedToken; 7 | use anchor_spl::token; 8 | use anchor_spl::token::{Mint, Token, TokenAccount}; 9 | 10 | // Burn droplets and redeem an NFT from the bucket in exchange 11 | pub fn redeem_nft(ctx: Context, swap: bool) -> Result<()> { 12 | // Not setting the flag cause the existing flag is what it should be 13 | ctx.accounts.swap_state.bump = *ctx.bumps.get("swap_state").unwrap(); 14 | ctx.accounts.swap_state.droplet_mint = ctx.accounts.droplet_mint.key(); 15 | ctx.accounts.swap_state.signer = ctx.accounts.signer.key(); 16 | 17 | let fee_basis_points; 18 | 19 | if swap { 20 | if ctx.accounts.swap_state.flag { 21 | // User passed swap=true and he's eligible 22 | fee_basis_points = SWAP_FEE_BASIS_POINTS; 23 | ctx.accounts.swap_state.flag = false; 24 | } else { 25 | // User passed swap=true but he's not eligible for swap yet 26 | return err!(SolventError::SwapNotAllowed); 27 | } 28 | } else { 29 | // User passed swap=false 30 | fee_basis_points = REDEEM_FEE_BASIS_POINTS; 31 | 32 | // Burn droplets from the signer's account 33 | let burn_droplets_ctx = CpiContext::new( 34 | ctx.accounts.token_program.to_account_info().clone(), 35 | token::Burn { 36 | mint: ctx.accounts.droplet_mint.to_account_info().clone(), 37 | from: ctx 38 | .accounts 39 | .signer_droplet_token_account 40 | .to_account_info() 41 | .clone(), 42 | authority: ctx.accounts.signer.to_account_info().clone(), 43 | }, 44 | ); 45 | token::burn( 46 | burn_droplets_ctx, 47 | DROPLETS_PER_NFT as u64 * LAMPORTS_PER_DROPLET, 48 | )?; 49 | } 50 | 51 | // Calculate redeem/swap fee 52 | let total_fee_amount = (DROPLETS_PER_NFT as u64) 53 | .checked_mul(LAMPORTS_PER_DROPLET as u64) 54 | .unwrap() 55 | .checked_mul(fee_basis_points as u64) 56 | .unwrap() 57 | .checked_div(10_000_u64) 58 | .unwrap(); 59 | 60 | let distributor_fee_amount = total_fee_amount 61 | .checked_mul(DISTRIBUTOR_FEE_BASIS_POINTS as u64) 62 | .unwrap() 63 | .checked_div(10_000_u64) 64 | .unwrap(); 65 | 66 | let solvent_treasury_fee_amount = total_fee_amount 67 | .checked_sub(distributor_fee_amount) 68 | .unwrap(); 69 | 70 | // Ensure correct fee calculation 71 | require!( 72 | distributor_fee_amount 73 | .checked_add(solvent_treasury_fee_amount as u64) 74 | .unwrap() 75 | == total_fee_amount, 76 | SolventError::FeeDistributionIncorrect 77 | ); 78 | 79 | // Transfer DISTRIBUTOR_FEE_PERCENTAGE % fee to distributor 80 | let transfer_fee_distributor_ctx = CpiContext::new( 81 | ctx.accounts.token_program.to_account_info().clone(), 82 | token::Transfer { 83 | from: ctx 84 | .accounts 85 | .signer_droplet_token_account 86 | .to_account_info() 87 | .clone(), 88 | to: ctx 89 | .accounts 90 | .distributor_droplet_token_account 91 | .to_account_info() 92 | .clone(), 93 | authority: ctx.accounts.signer.to_account_info().clone(), 94 | }, 95 | ); 96 | token::transfer(transfer_fee_distributor_ctx, distributor_fee_amount)?; 97 | 98 | // Transfer (100 - DISTRIBUTOR_FEE_PERCENTAGE) % fee to solvent treasury 99 | let transfer_fee_treasury_ctx = CpiContext::new( 100 | ctx.accounts.token_program.to_account_info().clone(), 101 | token::Transfer { 102 | from: ctx 103 | .accounts 104 | .signer_droplet_token_account 105 | .to_account_info() 106 | .clone(), 107 | to: ctx 108 | .accounts 109 | .solvent_treasury_droplet_token_account 110 | .to_account_info() 111 | .clone(), 112 | authority: ctx.accounts.signer.to_account_info().clone(), 113 | }, 114 | ); 115 | token::transfer(transfer_fee_treasury_ctx, solvent_treasury_fee_amount)?; 116 | 117 | // Get Solvent authority signer seeds 118 | let solvent_authority_bump = *ctx.bumps.get("solvent_authority").unwrap(); 119 | let solvent_authority_seeds = &[SOLVENT_AUTHORITY_SEED.as_bytes(), &[solvent_authority_bump]]; 120 | let solvent_authority_signer_seeds = &[&solvent_authority_seeds[..]]; 121 | 122 | // Transfer NFT to destination account 123 | let transfer_nft_ctx = CpiContext::new_with_signer( 124 | ctx.accounts.token_program.to_account_info().clone(), 125 | token::Transfer { 126 | from: ctx 127 | .accounts 128 | .solvent_nft_token_account 129 | .to_account_info() 130 | .clone(), 131 | to: ctx 132 | .accounts 133 | .destination_nft_token_account 134 | .to_account_info() 135 | .clone(), 136 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 137 | }, 138 | solvent_authority_signer_seeds, 139 | ); 140 | token::transfer(transfer_nft_ctx, 1)?; 141 | 142 | // Close bucket's NFT token account to reclaim lamports 143 | let close_nft_token_account_ctx = CpiContext::new_with_signer( 144 | ctx.accounts.token_program.to_account_info().clone(), 145 | token::CloseAccount { 146 | account: ctx 147 | .accounts 148 | .solvent_nft_token_account 149 | .to_account_info() 150 | .clone(), 151 | destination: ctx.accounts.signer.to_account_info().clone(), 152 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 153 | }, 154 | solvent_authority_signer_seeds, 155 | ); 156 | token::close_account(close_nft_token_account_ctx)?; 157 | 158 | // Decrement counter 159 | ctx.accounts.bucket_state.num_nfts_in_bucket = ctx 160 | .accounts 161 | .bucket_state 162 | .num_nfts_in_bucket 163 | .checked_sub(1) 164 | .unwrap(); 165 | 166 | // Emit success event 167 | emit!(RedeemNftEvent { 168 | droplet_mint: ctx.accounts.droplet_mint.key(), 169 | nft_mint: ctx.accounts.nft_mint.key(), 170 | signer: ctx.accounts.signer.key(), 171 | destination_nft_token_account: ctx.accounts.destination_nft_token_account.key(), 172 | signer_droplet_token_account: ctx.accounts.signer_droplet_token_account.key(), 173 | swap, 174 | num_nfts_in_bucket: ctx.accounts.bucket_state.num_nfts_in_bucket 175 | }); 176 | 177 | Ok(()) 178 | } 179 | 180 | #[derive(Accounts)] 181 | pub struct RedeemNft<'info> { 182 | #[account(mut)] 183 | pub signer: Signer<'info>, 184 | 185 | #[account( 186 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 187 | bump, 188 | )] 189 | /// CHECK: Safe because this read-only account only gets used as a constraint 190 | pub solvent_authority: UncheckedAccount<'info>, 191 | 192 | /// CHECK: Safe because we are using this account just for a constraint 193 | pub distributor: UncheckedAccount<'info>, 194 | 195 | #[account( 196 | init_if_needed, 197 | payer = signer, 198 | associated_token::mint = droplet_mint, 199 | associated_token::authority = distributor 200 | )] 201 | pub distributor_droplet_token_account: Box>, 202 | 203 | #[account( 204 | mut, 205 | seeds = [ 206 | droplet_mint.key().as_ref(), 207 | BUCKET_SEED.as_bytes() 208 | ], 209 | bump = bucket_state.bump, 210 | has_one = droplet_mint 211 | )] 212 | pub bucket_state: Box>, 213 | 214 | #[account( 215 | mut, 216 | seeds = [ 217 | droplet_mint.key().as_ref(), 218 | nft_mint.key().as_ref(), 219 | DEPOSIT_SEED.as_bytes() 220 | ], 221 | bump = deposit_state.bump, 222 | close = signer, 223 | has_one = nft_mint, 224 | has_one = droplet_mint 225 | )] 226 | pub deposit_state: Box>, 227 | 228 | #[account( 229 | init_if_needed, 230 | seeds = [ 231 | droplet_mint.key().as_ref(), 232 | signer.key().as_ref(), 233 | SWAP_SEED.as_bytes() 234 | ], 235 | bump, 236 | payer = signer, 237 | space = SwapState::LEN 238 | )] 239 | pub swap_state: Box>, 240 | 241 | #[account(mut)] 242 | pub droplet_mint: Box>, 243 | 244 | pub nft_mint: Box>, 245 | 246 | #[account( 247 | mut, 248 | address = get_associated_token_address(solvent_authority.key, &nft_mint.key()), 249 | )] 250 | pub solvent_nft_token_account: Box>, 251 | 252 | #[account( 253 | mut, 254 | constraint = destination_nft_token_account.mint == nft_mint.key(), 255 | )] 256 | pub destination_nft_token_account: Box>, 257 | 258 | #[account(address = SOLVENT_CORE_TREASURY @ SolventError::SolventTreasuryInvalid)] 259 | /// CHECK: Safe because there are enough constraints set 260 | pub solvent_treasury: UncheckedAccount<'info>, 261 | 262 | #[account( 263 | init_if_needed, 264 | payer = signer, 265 | associated_token::mint = droplet_mint, 266 | associated_token::authority = solvent_treasury, 267 | )] 268 | pub solvent_treasury_droplet_token_account: Box>, 269 | 270 | #[account( 271 | mut, 272 | constraint = signer_droplet_token_account.mint == droplet_mint.key() 273 | )] 274 | pub signer_droplet_token_account: Box>, 275 | 276 | // Solana ecosystem program addresses 277 | pub token_program: Program<'info, Token>, 278 | pub system_program: Program<'info, System>, 279 | pub associated_token_program: Program<'info, AssociatedToken>, 280 | pub rent: Sysvar<'info, Rent>, 281 | } 282 | 283 | #[event] 284 | pub struct RedeemNftEvent { 285 | pub signer: Pubkey, 286 | pub nft_mint: Pubkey, 287 | pub droplet_mint: Pubkey, 288 | pub signer_droplet_token_account: Pubkey, 289 | pub destination_nft_token_account: Pubkey, 290 | pub swap: bool, 291 | // Counters 292 | pub num_nfts_in_bucket: u16, 293 | } 294 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/set_locking_enabled.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::*; 2 | use crate::errors::*; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::token::Mint; 6 | 7 | // Update the parameters for an NFT locker 8 | pub fn set_locking_enabled(ctx: Context, flag: bool) -> Result<()> { 9 | ctx.accounts.bucket_state.is_locking_enabled = flag; 10 | 11 | // Emit success event 12 | emit!(SetLockingEnabledEvent { 13 | droplet_mint: ctx.accounts.droplet_mint.key(), 14 | flag 15 | }); 16 | 17 | Ok(()) 18 | } 19 | 20 | #[derive(Accounts)] 21 | pub struct SetLockingEnabled<'info> { 22 | #[account(address = SOLVENT_ADMIN @ SolventError::AdminAccessUnauthorized)] 23 | pub signer: Signer<'info>, 24 | 25 | #[account( 26 | mut, 27 | seeds = [ 28 | droplet_mint.key().as_ref(), 29 | BUCKET_SEED.as_bytes() 30 | ], 31 | bump = bucket_state.bump, 32 | has_one = droplet_mint 33 | )] 34 | pub bucket_state: Account<'info, BucketStateV3>, 35 | 36 | pub droplet_mint: Account<'info, Mint>, 37 | } 38 | 39 | #[event] 40 | pub struct SetLockingEnabledEvent { 41 | pub droplet_mint: Pubkey, 42 | pub flag: bool, 43 | } 44 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/set_staking_enabled.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::*; 2 | use crate::errors::*; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::token::Mint; 6 | 7 | // Update the parameters for an NFT locker 8 | pub fn set_staking_enabled(ctx: Context, flag: bool) -> Result<()> { 9 | ctx.accounts.bucket_state.is_staking_enabled = flag; 10 | 11 | // Emit success event 12 | emit!(SetStakingEnabledEvent { 13 | droplet_mint: ctx.accounts.droplet_mint.key(), 14 | flag 15 | }); 16 | 17 | Ok(()) 18 | } 19 | 20 | #[derive(Accounts)] 21 | pub struct SetStakingEnabled<'info> { 22 | #[account(address = SOLVENT_ADMIN @ SolventError::AdminAccessUnauthorized)] 23 | pub signer: Signer<'info>, 24 | 25 | #[account( 26 | mut, 27 | seeds = [ 28 | droplet_mint.key().as_ref(), 29 | BUCKET_SEED.as_bytes() 30 | ], 31 | bump = bucket_state.bump, 32 | has_one = droplet_mint 33 | )] 34 | pub bucket_state: Account<'info, BucketStateV3>, 35 | 36 | pub droplet_mint: Account<'info, Mint>, 37 | } 38 | 39 | #[event] 40 | pub struct SetStakingEnabledEvent { 41 | pub droplet_mint: Pubkey, 42 | pub flag: bool, 43 | } 44 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/start_migration.rs: -------------------------------------------------------------------------------- 1 | // TODO: To remove after migration is done 2 | 3 | use crate::constants::*; 4 | use crate::errors::*; 5 | use crate::state::*; 6 | use anchor_lang::prelude::*; 7 | use anchor_spl::token::Mint; 8 | 9 | // Create the migration state account 10 | pub fn start_migration(ctx: Context) -> Result<()> { 11 | // Set MigrationState account contents 12 | *ctx.accounts.migration_state = MigrationState { 13 | bump: *ctx.bumps.get("migration_state").unwrap(), 14 | droplet_mint_old: ctx.accounts.droplet_mint_old.key(), 15 | droplet_mint_new: ctx.accounts.droplet_mint_new.key(), 16 | }; 17 | 18 | // Emit success event 19 | emit!(StartMigrationEvent { 20 | droplet_mint_old: ctx.accounts.droplet_mint_old.key(), 21 | droplet_mint_new: ctx.accounts.droplet_mint_new.key(), 22 | }); 23 | 24 | Ok(()) 25 | } 26 | 27 | #[derive(Accounts)] 28 | pub struct StartMigration<'info> { 29 | #[account(mut, address = SOLVENT_ADMIN @ SolventError::AdminAccessUnauthorized)] 30 | pub signer: Signer<'info>, 31 | 32 | #[account( 33 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 34 | bump, 35 | )] 36 | /// CHECK: Safe because this read-only account only gets used as a constraint 37 | pub solvent_authority: UncheckedAccount<'info>, 38 | 39 | #[account( 40 | init, 41 | seeds = [ 42 | droplet_mint_new.key().as_ref(), 43 | MIGRATION_SEED.as_bytes() 44 | ], 45 | bump, 46 | payer = signer, 47 | space = MigrationState::LEN, 48 | )] 49 | pub migration_state: Account<'info, MigrationState>, 50 | 51 | //#[soteria(ignore_untrustful)] 52 | pub droplet_mint_old: Account<'info, Mint>, 53 | 54 | #[account(constraint = droplet_mint_new.mint_authority.unwrap() == solvent_authority.key())] 55 | pub droplet_mint_new: Account<'info, Mint>, 56 | 57 | // Solana ecosystem program addresses 58 | pub system_program: Program<'info, System>, 59 | } 60 | 61 | #[event] 62 | pub struct StartMigrationEvent { 63 | pub droplet_mint_old: Pubkey, 64 | pub droplet_mint_new: Pubkey, 65 | } 66 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/unlock_nft.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::*; 2 | use crate::errors::*; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::associated_token::{get_associated_token_address, AssociatedToken}; 6 | use anchor_spl::token; 7 | use anchor_spl::token::{Mint, Token, TokenAccount}; 8 | 9 | // Unlock a locked NFT from the locker by burning droplets 10 | pub fn unlock_nft(ctx: Context) -> Result<()> { 11 | let clock: Clock = Clock::get().unwrap(); 12 | 13 | // Verify that the loan has not defaulted 14 | require!( 15 | clock.unix_timestamp as u64 16 | <= ctx 17 | .accounts 18 | .locker_state 19 | .creation_timestamp 20 | .checked_add(ctx.accounts.locker_state.duration) 21 | .unwrap(), 22 | SolventError::LockerExpired 23 | ); 24 | 25 | // Calculate interest amount 26 | let interest = ((clock.unix_timestamp as u64) 27 | .checked_sub(ctx.accounts.locker_state.creation_timestamp) 28 | .unwrap()) 29 | .checked_mul(ctx.accounts.locker_state.max_interest_payable) 30 | .unwrap() 31 | .checked_div(ctx.accounts.locker_state.duration) 32 | .unwrap(); 33 | 34 | // Ensure user has enough droplets 35 | require!( 36 | ctx.accounts.signer_droplet_token_account.amount 37 | >= ctx 38 | .accounts 39 | .locker_state 40 | .principal_amount 41 | .checked_add(interest) 42 | .unwrap(), 43 | SolventError::DropletsInsufficient 44 | ); 45 | 46 | // Burn droplets from the signer's account 47 | let burn_droplets_ctx = CpiContext::new( 48 | ctx.accounts.token_program.to_account_info().clone(), 49 | token::Burn { 50 | mint: ctx.accounts.droplet_mint.to_account_info().clone(), 51 | from: ctx 52 | .accounts 53 | .signer_droplet_token_account 54 | .to_account_info() 55 | .clone(), 56 | authority: ctx.accounts.signer.to_account_info().clone(), 57 | }, 58 | ); 59 | token::burn( 60 | burn_droplets_ctx, 61 | ctx.accounts.locker_state.principal_amount as u64, 62 | )?; 63 | 64 | // Transfer interest from the depositor's account to the Solvent treasury 65 | let transfer_droplets_ctx = CpiContext::new( 66 | ctx.accounts.token_program.to_account_info().clone(), 67 | token::Transfer { 68 | from: ctx 69 | .accounts 70 | .signer_droplet_token_account 71 | .to_account_info() 72 | .clone(), 73 | to: ctx 74 | .accounts 75 | .solvent_treasury_droplet_token_account 76 | .to_account_info() 77 | .clone(), 78 | authority: ctx.accounts.signer.to_account_info().clone(), 79 | }, 80 | ); 81 | token::transfer(transfer_droplets_ctx, interest as u64)?; 82 | 83 | // Get Solvent authority signer seeds 84 | let solvent_authority_bump = *ctx.bumps.get("solvent_authority").unwrap(); 85 | let solvent_authority_seeds = &[SOLVENT_AUTHORITY_SEED.as_bytes(), &[solvent_authority_bump]]; 86 | let solvent_authority_signer_seeds = &[&solvent_authority_seeds[..]]; 87 | 88 | // Transfer NFT to destination account 89 | let transfer_nft_ctx = CpiContext::new_with_signer( 90 | ctx.accounts.token_program.to_account_info().clone(), 91 | token::Transfer { 92 | from: ctx 93 | .accounts 94 | .solvent_nft_token_account 95 | .to_account_info() 96 | .clone(), 97 | to: ctx 98 | .accounts 99 | .destination_nft_token_account 100 | .to_account_info() 101 | .clone(), 102 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 103 | }, 104 | solvent_authority_signer_seeds, 105 | ); 106 | token::transfer(transfer_nft_ctx, 1)?; 107 | 108 | // Close locker's NFT token account to reclaim lamports 109 | let close_nft_token_account_ctx = CpiContext::new_with_signer( 110 | ctx.accounts.token_program.to_account_info().clone(), 111 | token::CloseAccount { 112 | account: ctx 113 | .accounts 114 | .solvent_nft_token_account 115 | .to_account_info() 116 | .clone(), 117 | destination: ctx.accounts.signer.to_account_info().clone(), 118 | authority: ctx.accounts.solvent_authority.to_account_info().clone(), 119 | }, 120 | solvent_authority_signer_seeds, 121 | ); 122 | token::close_account(close_nft_token_account_ctx)?; 123 | 124 | // Decrement counter 125 | ctx.accounts.bucket_state.num_nfts_in_lockers = ctx 126 | .accounts 127 | .bucket_state 128 | .num_nfts_in_lockers 129 | .checked_sub(1) 130 | .unwrap(); 131 | 132 | // Emit success event 133 | emit!(UnlockNftEvent { 134 | droplet_mint: ctx.accounts.droplet_mint.key(), 135 | nft_mint: ctx.accounts.nft_mint.key(), 136 | signer: ctx.accounts.signer.key(), 137 | destination_nft_token_account: ctx.accounts.destination_nft_token_account.key(), 138 | signer_droplet_token_account: ctx.accounts.signer_droplet_token_account.key(), 139 | interest, 140 | // LockerState params 141 | creation_timestamp: ctx.accounts.locker_state.creation_timestamp, 142 | duration: ctx.accounts.locker_state.duration, 143 | principal_amount: ctx.accounts.locker_state.principal_amount, 144 | max_interest_payable: ctx.accounts.locker_state.max_interest_payable, 145 | // Counters 146 | num_nfts_in_bucket: ctx.accounts.bucket_state.num_nfts_in_bucket, 147 | num_nfts_in_lockers: ctx.accounts.bucket_state.num_nfts_in_lockers, 148 | }); 149 | 150 | Ok(()) 151 | } 152 | 153 | #[derive(Accounts)] 154 | pub struct UnlockNft<'info> { 155 | #[account(mut)] 156 | pub signer: Signer<'info>, 157 | 158 | #[account( 159 | mut, 160 | seeds = [ 161 | droplet_mint.key().as_ref(), 162 | nft_mint.key().as_ref(), 163 | LOCKER_SEED.as_bytes() 164 | ], 165 | bump = locker_state.bump, 166 | close = signer, 167 | constraint = locker_state.depositor == signer.key() @ SolventError::LockerAccessUnauthorized, 168 | has_one = droplet_mint, 169 | has_one = nft_mint 170 | )] 171 | pub locker_state: Account<'info, LockerState>, 172 | 173 | #[account( 174 | seeds = [SOLVENT_AUTHORITY_SEED.as_bytes()], 175 | bump, 176 | )] 177 | /// CHECK: Safe because this read-only account only gets used as a constraint 178 | pub solvent_authority: UncheckedAccount<'info>, 179 | 180 | #[account( 181 | mut, 182 | seeds = [ 183 | droplet_mint.key().as_ref(), 184 | BUCKET_SEED.as_bytes() 185 | ], 186 | bump = bucket_state.bump, 187 | has_one = droplet_mint 188 | )] 189 | pub bucket_state: Box>, 190 | 191 | pub nft_mint: Account<'info, Mint>, 192 | 193 | #[account( 194 | mut, 195 | address = get_associated_token_address(solvent_authority.key, &nft_mint.key()), 196 | )] 197 | pub solvent_nft_token_account: Box>, 198 | 199 | #[account( 200 | mut, 201 | constraint = signer_droplet_token_account.mint == droplet_mint.key() 202 | )] 203 | pub signer_droplet_token_account: Box>, 204 | 205 | #[account( 206 | mut, 207 | constraint = destination_nft_token_account.mint == nft_mint.key() 208 | )] 209 | pub destination_nft_token_account: Box>, 210 | 211 | #[account(address = SOLVENT_LOCKERS_TREASURY @ SolventError::SolventTreasuryInvalid)] 212 | /// CHECK: Safe because this read-only account only gets used as a constraint 213 | pub solvent_treasury: UncheckedAccount<'info>, 214 | 215 | #[account( 216 | init_if_needed, 217 | payer = signer, 218 | associated_token::mint = droplet_mint, 219 | associated_token::authority = solvent_treasury, 220 | )] 221 | pub solvent_treasury_droplet_token_account: Box>, 222 | 223 | #[account(mut)] 224 | pub droplet_mint: Account<'info, Mint>, 225 | 226 | // Solana ecosystem program addresses 227 | pub token_program: Program<'info, Token>, 228 | pub system_program: Program<'info, System>, 229 | pub associated_token_program: Program<'info, AssociatedToken>, 230 | pub rent: Sysvar<'info, Rent>, 231 | } 232 | 233 | #[event] 234 | pub struct UnlockNftEvent { 235 | pub signer: Pubkey, 236 | pub nft_mint: Pubkey, 237 | pub droplet_mint: Pubkey, 238 | pub signer_droplet_token_account: Pubkey, 239 | pub destination_nft_token_account: Pubkey, 240 | pub interest: u64, 241 | // LockerState params 242 | pub creation_timestamp: u64, 243 | pub duration: u64, 244 | pub principal_amount: u64, 245 | pub max_interest_payable: u64, 246 | // Counters 247 | pub num_nfts_in_bucket: u16, 248 | pub num_nfts_in_lockers: u16, 249 | } 250 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/update_collection_info.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use crate::constants::*; 3 | use crate::errors::*; 4 | use crate::state::*; 5 | use anchor_lang::prelude::*; 6 | use anchor_spl::token::Mint; 7 | 8 | // Update NFT collection info of a bucket 9 | pub fn update_collection_info( 10 | ctx: Context, 11 | collection_info: CollectionInfo, 12 | ) -> Result<()> { 13 | // Make sure collection info is valid 14 | validate_collection_info(&collection_info)?; 15 | 16 | // Update BucketState account with new collection info 17 | ctx.accounts.bucket_state.collection_info = collection_info.clone(); 18 | 19 | // Emit success event 20 | emit!(UpdateCollectionInfoEvent { 21 | droplet_mint: ctx.accounts.droplet_mint.key(), 22 | collection_info 23 | }); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[derive(Accounts)] 29 | pub struct UpdateCollectionInfo<'info> { 30 | #[account(address = SOLVENT_ADMIN @ SolventError::AdminAccessUnauthorized)] 31 | pub signer: Signer<'info>, 32 | 33 | #[account( 34 | mut, 35 | seeds = [ 36 | droplet_mint.key().as_ref(), 37 | BUCKET_SEED.as_bytes() 38 | ], 39 | bump = bucket_state.bump, 40 | has_one = droplet_mint 41 | )] 42 | pub bucket_state: Account<'info, BucketStateV3>, 43 | 44 | pub droplet_mint: Account<'info, Mint>, 45 | 46 | // Solana ecosystem program addresses 47 | pub system_program: Program<'info, System>, 48 | } 49 | 50 | #[event] 51 | pub struct UpdateCollectionInfoEvent { 52 | pub droplet_mint: Pubkey, 53 | pub collection_info: CollectionInfo, 54 | } 55 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/update_locking_params.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::*; 2 | use crate::errors::*; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::token::Mint; 6 | 7 | // Update the parameters for an NFT locker 8 | pub fn update_locking_params( 9 | ctx: Context, 10 | max_locker_duration: Option, 11 | interest_scaler: Option, 12 | ) -> Result<()> { 13 | // Update locking params if they are supplied 14 | 15 | if let Some(interest_scaler) = interest_scaler { 16 | // Ensure interest_scaler is smaller than the max value 17 | require!( 18 | interest_scaler <= MAX_INTEREST_SCALER, 19 | SolventError::InterestScalerInvalid 20 | ); 21 | ctx.accounts.bucket_state.interest_scaler = interest_scaler; 22 | } 23 | 24 | if let Some(max_locker_duration) = max_locker_duration { 25 | ctx.accounts.bucket_state.max_locker_duration = max_locker_duration; 26 | } 27 | 28 | // Emit success event 29 | emit!(UpdateLockingParamsEvent { 30 | droplet_mint: ctx.accounts.droplet_mint.key(), 31 | max_locker_duration, 32 | interest_scaler, 33 | }); 34 | 35 | Ok(()) 36 | } 37 | 38 | #[derive(Accounts)] 39 | pub struct UpdateLockingParams<'info> { 40 | #[account(address = SOLVENT_ADMIN @ SolventError::AdminAccessUnauthorized)] 41 | pub signer: Signer<'info>, 42 | 43 | #[account( 44 | mut, 45 | seeds = [ 46 | droplet_mint.key().as_ref(), 47 | BUCKET_SEED.as_bytes() 48 | ], 49 | bump = bucket_state.bump, 50 | has_one = droplet_mint 51 | )] 52 | pub bucket_state: Account<'info, BucketStateV3>, 53 | 54 | pub droplet_mint: Account<'info, Mint>, 55 | } 56 | 57 | #[event] 58 | pub struct UpdateLockingParamsEvent { 59 | pub droplet_mint: Pubkey, 60 | pub max_locker_duration: Option, 61 | pub interest_scaler: Option, 62 | } 63 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/instructions/update_staking_params.rs: -------------------------------------------------------------------------------- 1 | use crate::common::validate_farm; 2 | use crate::constants::*; 3 | use crate::errors::*; 4 | use crate::state::*; 5 | use anchor_lang::prelude::*; 6 | use anchor_spl::token::Mint; 7 | 8 | pub fn update_staking_params(ctx: Context) -> Result<()> { 9 | // Store farming related pubkeys in bucket state 10 | ctx.accounts.bucket_state.staking_params = Some(StakingParams { 11 | gembank_program: ctx.accounts.gembank_program.key(), 12 | gemfarm_program: ctx.accounts.gemfarm_program.key(), 13 | gemworks_farm: ctx.accounts.gemworks_farm.key(), 14 | gemworks_fee_account: ctx.accounts.gemworks_fee_account.key(), 15 | }); 16 | 17 | // Emit success event 18 | emit!(UpdateStakingParamsEvent { 19 | droplet_mint: ctx.accounts.droplet_mint.key(), 20 | gembank_program: ctx.accounts.gembank_program.key(), 21 | gemfarm_program: ctx.accounts.gemfarm_program.key(), 22 | gemworks_farm: ctx.accounts.gemworks_farm.key(), 23 | gemworks_fee_account: ctx.accounts.gemworks_fee_account.key(), 24 | }); 25 | 26 | Ok(()) 27 | } 28 | 29 | #[derive(Accounts)] 30 | pub struct UpdateStakingParams<'info> { 31 | #[account(address = SOLVENT_ADMIN @ SolventError::AdminAccessUnauthorized)] 32 | pub signer: Signer<'info>, 33 | 34 | #[account( 35 | mut, 36 | seeds = [ 37 | droplet_mint.key().as_ref(), 38 | BUCKET_SEED.as_bytes() 39 | ], 40 | bump = bucket_state.bump, 41 | has_one = droplet_mint 42 | )] 43 | pub bucket_state: Box>, 44 | 45 | pub droplet_mint: Account<'info, Mint>, 46 | 47 | /// CHECK: 48 | //#[soteria(ignore_untrustful)] 49 | #[account( 50 | owner = gemfarm_program.key(), 51 | constraint = validate_farm(&gemworks_farm)? @ SolventError::FarmConfigInvalid 52 | )] 53 | pub gemworks_farm: UncheckedAccount<'info>, 54 | 55 | /// CHECK: 56 | //#[soteria(ignore_untrustful)] 57 | #[account(executable)] 58 | pub gembank_program: UncheckedAccount<'info>, 59 | 60 | /// CHECK: 61 | //#[soteria(ignore_untrustful)] 62 | #[account(executable)] 63 | pub gemfarm_program: UncheckedAccount<'info>, 64 | 65 | /// CHECK: 66 | //#[soteria(ignore_untrustful)] 67 | pub gemworks_fee_account: UncheckedAccount<'info>, 68 | 69 | // Solana ecosystem program addresses 70 | pub system_program: Program<'info, System>, 71 | } 72 | 73 | #[event] 74 | pub struct UpdateStakingParamsEvent { 75 | pub droplet_mint: Pubkey, 76 | pub gembank_program: Pubkey, 77 | pub gemfarm_program: Pubkey, 78 | pub gemworks_farm: Pubkey, 79 | pub gemworks_fee_account: Pubkey, 80 | } 81 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | pub mod constants; 3 | pub mod errors; 4 | pub mod instructions; 5 | pub mod state; 6 | 7 | use anchor_lang::prelude::*; 8 | use common::*; 9 | use instructions::*; 10 | 11 | declare_id!("SVTy4zMgDPExf1RaJdoCo5HvuyxrxdRsqF1uf2Rcd7J"); 12 | 13 | #[program] 14 | pub mod solvent_protocol { 15 | 16 | use super::*; 17 | 18 | // Create a bucket for an NFT collection 19 | pub fn create_bucket( 20 | ctx: Context, 21 | collection_info: CollectionInfo, 22 | ) -> Result<()> { 23 | instructions::create_bucket(ctx, collection_info) 24 | } 25 | 26 | // Deposit an NFT into a bucket 27 | pub fn deposit_nft( 28 | ctx: Context, 29 | swap: bool, 30 | whitelist_proof: Option>, 31 | ) -> Result<()> { 32 | instructions::deposit_nft(ctx, swap, whitelist_proof) 33 | } 34 | 35 | // Redeem an NFT from the bucket 36 | pub fn redeem_nft(ctx: Context, swap: bool) -> Result<()> { 37 | instructions::redeem_nft(ctx, swap) 38 | } 39 | 40 | // Lock an NFT into a locker and get droplets in exchange 41 | pub fn lock_nft( 42 | ctx: Context, 43 | duration: u64, 44 | whitelist_proof: Option>, 45 | ) -> Result { 46 | instructions::lock_nft(ctx, duration, whitelist_proof) 47 | } 48 | 49 | // Unlock a locked NFT from the locker by burning droplets 50 | pub fn unlock_nft(ctx: Context) -> Result<()> { 51 | instructions::unlock_nft(ctx) 52 | } 53 | 54 | // Liquidate a locked NFT position that is defaulted 55 | pub fn liquidate_locker(ctx: Context) -> Result<()> { 56 | instructions::liquidate_locker(ctx) 57 | } 58 | 59 | // Update the parameters for NFT locking feature 60 | pub fn update_locking_params( 61 | ctx: Context, 62 | max_locker_duration: Option, 63 | interest_scaler: Option, 64 | ) -> Result<()> { 65 | instructions::update_locking_params(ctx, max_locker_duration, interest_scaler) 66 | } 67 | 68 | pub fn update_staking_params(ctx: Context) -> Result<()> { 69 | instructions::update_staking_params(ctx) 70 | } 71 | 72 | pub fn set_locking_enabled(ctx: Context, flag: bool) -> Result<()> { 73 | instructions::set_locking_enabled(ctx, flag) 74 | } 75 | 76 | pub fn set_staking_enabled(ctx: Context, flag: bool) -> Result<()> { 77 | instructions::set_staking_enabled(ctx, flag) 78 | } 79 | 80 | pub fn stake_nft(ctx: Context) -> Result<()> { 81 | instructions::stake_nft(ctx) 82 | } 83 | 84 | pub fn unstake_nft(ctx: Context) -> Result<()> { 85 | instructions::unstake_nft(ctx) 86 | } 87 | 88 | // Create the migration state account 89 | pub fn start_migration(ctx: Context) -> Result<()> { 90 | instructions::start_migration(ctx) 91 | } 92 | 93 | // Take old droplets from user and mint to him new droplets in exchange 94 | pub fn migrate_droplets(ctx: Context) -> Result<()> { 95 | instructions::migrate_droplets(ctx) 96 | } 97 | 98 | pub fn migrate_nft( 99 | ctx: Context, 100 | whitelist_proof: Option>, 101 | ) -> Result<()> { 102 | instructions::migrate_nft(ctx, whitelist_proof) 103 | } 104 | 105 | pub fn claim_balance(ctx: Context) -> Result<()> { 106 | instructions::claim_balance(ctx) 107 | } 108 | 109 | pub fn update_collection_info( 110 | ctx: Context, 111 | collection_info: CollectionInfo, 112 | ) -> Result<()> { 113 | instructions::update_collection_info(ctx, collection_info) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /programs/solvent-protocol/src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::common::CollectionInfo; 2 | use anchor_lang::prelude::*; 3 | 4 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Copy)] 5 | pub struct StakingParams { 6 | pub gembank_program: Pubkey, 7 | pub gemfarm_program: Pubkey, 8 | pub gemworks_farm: Pubkey, 9 | pub gemworks_fee_account: Pubkey, 10 | } 11 | 12 | impl StakingParams { 13 | // 4 Pubkeys 14 | pub const LEN: usize = 4 * 32; 15 | } 16 | 17 | // BucketStateV3 is a PDA of droplet_mint and "bucket-seed-v3" 18 | #[account] 19 | pub struct BucketStateV3 { 20 | // Basic information about the bucket 21 | pub bump: u8, 22 | pub droplet_mint: Pubkey, 23 | pub collection_info: CollectionInfo, 24 | 25 | // NFT lockers feature related params 26 | pub is_locking_enabled: bool, 27 | pub max_locker_duration: u64, 28 | pub interest_scaler: u8, 29 | 30 | // Counters 31 | pub num_nfts_in_bucket: u16, 32 | pub num_nfts_in_lockers: u16, 33 | 34 | // NFT staking related params 35 | pub is_staking_enabled: bool, 36 | // Either all of the 5 pubkeys should be there, or none of them 37 | pub staking_params: Option, 38 | } 39 | 40 | impl BucketStateV3 { 41 | pub const LEN: usize = 42 | // Discriminator, 1 Pubkey, 1 CollectionInfo, 2 bools, 1 u64, 2 u16s, 1 u8 + 1 Option 43 | 8 + 32 + CollectionInfo::LEN + 2 + 8 + (2 * 2) + 1 + (1 + StakingParams::LEN); 44 | } 45 | 46 | // LockerState is a PDA of droplet_mint, nft_mint, and "locker-seed" 47 | #[account] 48 | pub struct LockerState { 49 | pub bump: u8, 50 | 51 | // Droplet mint associated with the collection 52 | pub droplet_mint: Pubkey, 53 | 54 | // User depositing NFT 55 | pub depositor: Pubkey, 56 | 57 | // NFT mint account 58 | pub nft_mint: Pubkey, 59 | 60 | // Timestamp of NFT deposit and locker creation 61 | pub creation_timestamp: u64, 62 | 63 | // Locker duration in seconds 64 | pub duration: u64, 65 | 66 | // The pricipal amout of the loan in droplet lamports 67 | pub principal_amount: u64, 68 | 69 | // Max interest payable in droplet lamports 70 | pub max_interest_payable: u64, 71 | } 72 | 73 | impl LockerState { 74 | // Discriminator, 1 u8, 3 Pubkeys, 4 u64s 75 | pub const LEN: usize = 8 + 1 + (3 * 32) + (4 * 8); 76 | } 77 | 78 | // DepositState is a PDA of droplet_mint, nft_mint, and "deposit-seed" 79 | #[account] 80 | pub struct DepositState { 81 | pub bump: u8, 82 | pub droplet_mint: Pubkey, 83 | pub nft_mint: Pubkey, 84 | } 85 | 86 | impl DepositState { 87 | // Discriminator, 1 u8, 2 Pubkeys 88 | pub const LEN: usize = 8 + 1 + (2 * 32); 89 | } 90 | 91 | // TODO: To remove after migration is done 92 | // MigrationState is a PDA of droplet_mint, and "migration-seed" 93 | #[account] 94 | pub struct MigrationState { 95 | pub bump: u8, 96 | pub droplet_mint_new: Pubkey, 97 | pub droplet_mint_old: Pubkey, 98 | } 99 | 100 | impl MigrationState { 101 | // Discriminator, 1 u8, 2 Pubkeys 102 | pub const LEN: usize = 8 + 1 + (2 * 32); 103 | } 104 | 105 | #[account] 106 | pub struct SwapState { 107 | pub bump: u8, 108 | pub droplet_mint: Pubkey, 109 | pub signer: Pubkey, 110 | pub flag: bool, 111 | } 112 | 113 | impl SwapState { 114 | // Discriminator, 1 u8, 2 Pubkeys, 1 bool 115 | pub const LEN: usize = 8 + 1 + (2 * 32) + 1; 116 | } 117 | -------------------------------------------------------------------------------- /programs/solvent-protocol/tests/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use ::solvent_protocol::common::*; 4 | use assert_panic::assert_panic; 5 | use test_case::test_case; 6 | 7 | #[test_case(CalculateLoanArgs{ 8 | max_locker_duration: 100, 9 | num_nfts_in_bucket: 10, 10 | num_nfts_in_lockers: 5, 11 | interest_scaler: 100, 12 | locker_duration: 10, 13 | }, Some(CalculateLoanResult { 14 | principal_amount: 9666666666, 15 | max_interest_payable: 333333334 16 | }) ; "success case 1")] 17 | #[test_case(CalculateLoanArgs{ 18 | max_locker_duration: 100, 19 | num_nfts_in_bucket: 10, 20 | num_nfts_in_lockers: 5, 21 | interest_scaler: 100, 22 | locker_duration: 10_000_000_000_000_000_000, 23 | }, None ; "overflow case 1")] 24 | #[test_case(CalculateLoanArgs{ 25 | max_locker_duration: 10_000_000_000_000_000_000, 26 | num_nfts_in_bucket: 10, 27 | num_nfts_in_lockers: 5, 28 | interest_scaler: 100, 29 | locker_duration: 10, 30 | }, None ; "overflow case 2")] 31 | fn test_calculate_loan(args: CalculateLoanArgs, expected_result: Option) { 32 | match expected_result { 33 | Some(expected_result) => { 34 | assert_eq!(calculate_loan(args), expected_result); 35 | } 36 | None => { 37 | assert_panic!({ 38 | calculate_loan(args); 39 | }); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | stable 2 | -------------------------------------------------------------------------------- /tests/genesis-programs/gem_bank.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solventprotocol/solvent-program/11454ab9ffcc8a95c346ddbe7523b4196fb103c3/tests/genesis-programs/gem_bank.so -------------------------------------------------------------------------------- /tests/genesis-programs/gem_farm.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solventprotocol/solvent-program/11454ab9ffcc8a95c346ddbe7523b4196fb103c3/tests/genesis-programs/gem_farm.so -------------------------------------------------------------------------------- /tests/genesis-programs/mpl_token_metadata.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solventprotocol/solvent-program/11454ab9ffcc8a95c346ddbe7523b4196fb103c3/tests/genesis-programs/mpl_token_metadata.so -------------------------------------------------------------------------------- /tests/genesis-programs/solvent.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solventprotocol/solvent-program/11454ab9ffcc8a95c346ddbe7523b4196fb103c3/tests/genesis-programs/solvent.so -------------------------------------------------------------------------------- /tests/keypairs/solvent-admin.json: -------------------------------------------------------------------------------- 1 | [178,164,142,167,71,246,108,75,117,252,20,42,18,228,76,105,31,30,70,197,0,114,48,206,196,204,198,25,181,121,208,0,184,34,84,147,62,187,209,190,80,218,22,62,214,19,158,254,177,14,96,54,161,72,144,118,162,161,163,210,46,177,149,91] -------------------------------------------------------------------------------- /tests/tests/buckets/create-bucket.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { createKeypair, getMerkleTree, mintNft } from "../../utils"; 3 | import { program, provider, BUCKET_SEED, smbMints } from "../common"; 4 | import { assert, expect } from "chai"; 5 | import { beforeEach } from "mocha"; 6 | 7 | describe("Creating buckets", () => { 8 | const nftSymbol = "DAPE"; 9 | 10 | let userKeypair: anchor.web3.Keypair, 11 | dropletMintKeypair: anchor.web3.Keypair, 12 | bucketStateAddress: anchor.web3.PublicKey; 13 | 14 | const { root: whitelistRoot } = getMerkleTree(smbMints); 15 | 16 | beforeEach(async () => { 17 | // An NFT enthusiast wants to create a bucket for an NFT collection 18 | userKeypair = await createKeypair(provider); 19 | 20 | // Create the bucket address 21 | dropletMintKeypair = new anchor.web3.Keypair(); 22 | 23 | [bucketStateAddress] = await anchor.web3.PublicKey.findProgramAddress( 24 | [dropletMintKeypair.publicKey.toBuffer(), BUCKET_SEED], 25 | program.programId 26 | ); 27 | }); 28 | 29 | describe("For Metaplex v1.0 collections", () => { 30 | // Addresses of verified creators of the NFT collection 31 | const creatorAddresses: anchor.web3.PublicKey[] = [...Array(5)].map( 32 | () => new anchor.web3.Keypair().publicKey 33 | ); 34 | 35 | it("can create a bucket", async () => { 36 | // Create bucket on Solvent 37 | await provider.connection.confirmTransaction( 38 | await program.methods 39 | // @ts-ignore 40 | .createBucket({ 41 | v1: { 42 | verifiedCreators: creatorAddresses, 43 | symbol: nftSymbol, 44 | whitelistRoot, 45 | }, 46 | }) 47 | .accounts({ 48 | signer: userKeypair.publicKey, 49 | dropletMint: dropletMintKeypair.publicKey, 50 | }) 51 | .signers([dropletMintKeypair, userKeypair]) 52 | .rpc() 53 | ); 54 | 55 | // Fetch BucketState account and assert it has correct data 56 | const bucketState = await program.account.bucketStateV3.fetch( 57 | bucketStateAddress 58 | ); 59 | // @ts-ignore 60 | assert.equal(bucketState.collectionInfo.v1.symbol, nftSymbol); 61 | assert(bucketState.dropletMint.equals(dropletMintKeypair.publicKey)); 62 | assert.equal( 63 | // @ts-ignore 64 | bucketState.collectionInfo.v1.verifiedCreators.length, 65 | creatorAddresses.length 66 | ); 67 | assert.sameMembers( 68 | // @ts-ignore 69 | bucketState.collectionInfo.v1.verifiedCreators.map((value) => 70 | value.toString() 71 | ), 72 | creatorAddresses.map((value) => value.toString()) 73 | ); 74 | expect(bucketState.isLockingEnabled).to.be.false; 75 | }); 76 | 77 | it("fails to create bucket when symbol is too long", async () => { 78 | try { 79 | // Create bucket on Solvent 80 | await program.methods 81 | // @ts-ignore 82 | .createBucket({ 83 | v1: { 84 | verifiedCreators: creatorAddresses, 85 | symbol: 86 | "an obvious exploit attempt where user is trying to overflow or something", 87 | whitelistRoot: [...whitelistRoot], 88 | }, 89 | }) 90 | .accounts({ 91 | signer: userKeypair.publicKey, 92 | dropletMint: dropletMintKeypair.publicKey, 93 | }) 94 | .signers([dropletMintKeypair, userKeypair]) 95 | .rpc(); 96 | } catch (error) { 97 | assert.include( 98 | error.message, 99 | "The NFT collection symbol you entered is invalid." 100 | ); 101 | return; 102 | } 103 | expect.fail( 104 | "Program did not fail while creating bucket with too long collection symbol." 105 | ); 106 | }); 107 | 108 | it("fails to create bucket when verified creators list is too long", async () => { 109 | try { 110 | // Create bucket on Solvent 111 | await program.methods 112 | // @ts-ignore 113 | .createBucket({ 114 | v1: { 115 | verifiedCreators: [...Array(10)].map( 116 | () => new anchor.web3.Keypair().publicKey 117 | ), 118 | symbol: "DAPE", 119 | whitelistRoot: [...whitelistRoot], 120 | }, 121 | }) 122 | .accounts({ 123 | signer: userKeypair.publicKey, 124 | dropletMint: dropletMintKeypair.publicKey, 125 | }) 126 | .signers([dropletMintKeypair, userKeypair]) 127 | .rpc(); 128 | } catch (error) { 129 | assert.include( 130 | error.message, 131 | "There should be 1 to 5 verified creators." 132 | ); 133 | return; 134 | } 135 | expect.fail( 136 | "Program did not fail while creating bucket with too many verified creators." 137 | ); 138 | }); 139 | 140 | it("fails to create bucket when no verified creator is supplied", async () => { 141 | try { 142 | // Create bucket on Solvent 143 | await program.methods 144 | // @ts-ignore 145 | .createBucket({ 146 | v1: { 147 | verifiedCreators: [], 148 | symbol: "DAPE", 149 | whitelistRoot: [...whitelistRoot], 150 | }, 151 | }) 152 | .accounts({ 153 | signer: userKeypair.publicKey, 154 | dropletMint: dropletMintKeypair.publicKey, 155 | }) 156 | .signers([dropletMintKeypair, userKeypair]) 157 | .rpc(); 158 | } catch (error) { 159 | assert.include( 160 | error.message, 161 | "There should be 1 to 5 verified creators." 162 | ); 163 | return; 164 | } 165 | expect.fail( 166 | "Program did not fail while creating bucket with no verified creator." 167 | ); 168 | }); 169 | }); 170 | 171 | describe("For Metaplex v1.1 collections", () => { 172 | let collectionMint: anchor.web3.PublicKey; 173 | 174 | beforeEach(async () => { 175 | // Create the collection NFT 176 | const collectionCreatorKeypair = await createKeypair(provider); 177 | const { mint } = await mintNft( 178 | provider, 179 | "DAPE", 180 | collectionCreatorKeypair, 181 | collectionCreatorKeypair.publicKey 182 | ); 183 | collectionMint = mint; 184 | }); 185 | 186 | it("can create a bucket", async () => { 187 | // Create bucket on Solvent 188 | await provider.connection.confirmTransaction( 189 | await program.methods 190 | // @ts-ignore 191 | .createBucket({ v2: { collectionMint } }) 192 | .accounts({ 193 | signer: userKeypair.publicKey, 194 | dropletMint: dropletMintKeypair.publicKey, 195 | }) 196 | .signers([dropletMintKeypair, userKeypair]) 197 | .rpc() 198 | ); 199 | 200 | // Fetch BucketState account and assert it has correct data 201 | const bucketState = await program.account.bucketStateV3.fetch( 202 | bucketStateAddress 203 | ); 204 | assert( 205 | // @ts-ignore 206 | bucketState.collectionInfo.v2.collectionMint.equals(collectionMint) 207 | ); 208 | assert(bucketState.dropletMint.equals(dropletMintKeypair.publicKey)); 209 | expect(bucketState.isLockingEnabled).to.be.false; 210 | }); 211 | 212 | afterEach(() => { 213 | collectionMint = undefined; 214 | }); 215 | }); 216 | 217 | afterEach(() => { 218 | userKeypair = undefined; 219 | dropletMintKeypair = undefined; 220 | bucketStateAddress = undefined; 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /tests/tests/buckets/update-collection-info.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { createKeypair, mintNft, verifyCollection } from "../../utils"; 3 | import { 4 | program, 5 | provider, 6 | BUCKET_SEED, 7 | SOLVENT_ADMIN, 8 | NftInfo, 9 | SOLVENT_AUTHORITY_SEED, 10 | SOLVENT_CORE_TREASURY, 11 | } from "../common"; 12 | import { expect } from "chai"; 13 | import { beforeEach } from "mocha"; 14 | import { 15 | getAccount, 16 | getAssociatedTokenAddress, 17 | getOrCreateAssociatedTokenAccount, 18 | } from "@solana/spl-token-latest"; 19 | 20 | describe("Updating collection info", () => { 21 | const nftSymbol = "DAPE"; 22 | 23 | let dropletMint: anchor.web3.PublicKey, 24 | bucketStateAddress: anchor.web3.PublicKey; 25 | 26 | let nftInfo: NftInfo; 27 | 28 | let solventAuthorityAddress: anchor.web3.PublicKey; 29 | 30 | before(async () => { 31 | [solventAuthorityAddress] = await anchor.web3.PublicKey.findProgramAddress( 32 | [SOLVENT_AUTHORITY_SEED], 33 | program.programId 34 | ); 35 | }); 36 | 37 | beforeEach(async () => { 38 | // An NFT enthusiast wants to create a bucket for an NFT collection 39 | const userKeypair = await createKeypair(provider); 40 | 41 | // Create the bucket address 42 | const dropletMintKeypair = new anchor.web3.Keypair(); 43 | dropletMint = dropletMintKeypair.publicKey; 44 | 45 | [bucketStateAddress] = await anchor.web3.PublicKey.findProgramAddress( 46 | [dropletMintKeypair.publicKey.toBuffer(), BUCKET_SEED], 47 | program.programId 48 | ); 49 | 50 | // Create the collection NFT 51 | const collectionCreatorKeypair = await createKeypair(provider); 52 | const { mint: collectionMint } = await mintNft( 53 | provider, 54 | nftSymbol, 55 | collectionCreatorKeypair, 56 | collectionCreatorKeypair.publicKey 57 | ); 58 | 59 | // Mint an NFT from that collection 60 | const creatorKeypair = await createKeypair(provider); 61 | const holderKeypair = await createKeypair(provider); 62 | const { metadata: nftMetadataAddress, mint: nftMintAddress } = 63 | await mintNft( 64 | provider, 65 | nftSymbol, 66 | creatorKeypair, 67 | holderKeypair.publicKey, 68 | collectionMint 69 | ); 70 | await verifyCollection( 71 | provider, 72 | nftMintAddress, 73 | collectionMint, 74 | collectionCreatorKeypair 75 | ); 76 | nftInfo = { 77 | nftMintAddress, 78 | nftMetadataAddress, 79 | holderKeypair, 80 | }; 81 | 82 | // Create bucket on Solvent 83 | await provider.connection.confirmTransaction( 84 | await program.methods 85 | // @ts-ignore 86 | .createBucket({ v2: { collectionMint } }) 87 | .accounts({ 88 | signer: userKeypair.publicKey, 89 | dropletMint: dropletMint, 90 | }) 91 | .signers([dropletMintKeypair, userKeypair]) 92 | .rpc() 93 | ); 94 | }); 95 | 96 | it("can update collection info", async () => { 97 | // Create a new collection NFT 98 | const collectionCreatorKeypair = await createKeypair(provider); 99 | const { mint: collectionMint } = await mintNft( 100 | provider, 101 | nftSymbol, 102 | collectionCreatorKeypair, 103 | collectionCreatorKeypair.publicKey 104 | ); 105 | 106 | // Update collection info 107 | await provider.connection.confirmTransaction( 108 | await program.methods 109 | // @ts-ignore 110 | .updateCollectionInfo({ v2: { collectionMint } }) 111 | .accounts({ 112 | signer: SOLVENT_ADMIN.publicKey, 113 | dropletMint: dropletMint, 114 | }) 115 | .signers([SOLVENT_ADMIN]) 116 | .rpc() 117 | ); 118 | 119 | // Fetch BucketState account and assert it has correct data 120 | const bucketState = await program.account.bucketStateV3.fetch( 121 | bucketStateAddress 122 | ); 123 | // @ts-ignore 124 | expect(bucketState.collectionInfo.v2.collectionMint.toBase58()).to.equal( 125 | collectionMint.toBase58() 126 | ); 127 | }); 128 | 129 | it("can deposit and redeem after updating collection info", async () => { 130 | // Deposit an NFT of collection mint 1 131 | const { 132 | nftMintAddress: nftMint1, 133 | nftMetadataAddress: nftMetadata1, 134 | holderKeypair, 135 | } = nftInfo; 136 | let holderNftTokenAccount = await getAssociatedTokenAddress( 137 | nftMint1, 138 | holderKeypair.publicKey 139 | ); 140 | let solventNftTokenAccount = await getAssociatedTokenAddress( 141 | nftMint1, 142 | solventAuthorityAddress, 143 | true 144 | ); 145 | let holderDropletTokenAccount = await getOrCreateAssociatedTokenAccount( 146 | provider.connection, 147 | holderKeypair, 148 | dropletMint, 149 | holderKeypair.publicKey 150 | ); 151 | 152 | // Deposit NFT into Solvent 153 | await provider.connection.confirmTransaction( 154 | await program.methods 155 | .depositNft(false, null) 156 | .accounts({ 157 | signer: holderKeypair.publicKey, 158 | dropletMint, 159 | nftMint: nftMint1, 160 | nftMetadata: nftMetadata1, 161 | signerNftTokenAccount: holderNftTokenAccount, 162 | solventNftTokenAccount, 163 | destinationDropletTokenAccount: holderDropletTokenAccount.address, 164 | }) 165 | .signers([holderKeypair]) 166 | .rpc() 167 | ); 168 | 169 | // Ensure user received 100 droplets for the NFT deposited 170 | holderDropletTokenAccount = await getAccount( 171 | provider.connection, 172 | holderDropletTokenAccount.address 173 | ); 174 | expect(holderDropletTokenAccount.amount).to.equal(BigInt(100 * 100000000)); 175 | 176 | // Create a new collection mint: collection mint 2 177 | const collectionCreatorKeypair = await createKeypair(provider); 178 | const { mint: collectionMint2 } = await mintNft( 179 | provider, 180 | nftSymbol, 181 | collectionCreatorKeypair, 182 | collectionCreatorKeypair.publicKey 183 | ); 184 | 185 | // Mint an NFT from collection mint 2 186 | const creatorKeypair = await createKeypair(provider); 187 | const { metadata: nftMetadata2, mint: nftMint2 } = await mintNft( 188 | provider, 189 | nftSymbol, 190 | creatorKeypair, 191 | holderKeypair.publicKey, 192 | collectionMint2 193 | ); 194 | await verifyCollection( 195 | provider, 196 | nftMint2, 197 | collectionMint2, 198 | collectionCreatorKeypair 199 | ); 200 | 201 | // Update collection info to use collection info 2 202 | await provider.connection.confirmTransaction( 203 | await program.methods 204 | // @ts-ignore 205 | .updateCollectionInfo({ v2: { collectionMint: collectionMint2 } }) 206 | .accounts({ 207 | signer: SOLVENT_ADMIN.publicKey, 208 | dropletMint: dropletMint, 209 | }) 210 | .signers([SOLVENT_ADMIN]) 211 | .rpc() 212 | ); 213 | 214 | // Fetch BucketState account and assert it has correct data 215 | const bucketState = await program.account.bucketStateV3.fetch( 216 | bucketStateAddress 217 | ); 218 | // @ts-ignore 219 | expect(bucketState.collectionInfo.v2.collectionMint.toBase58()).to.equal( 220 | collectionMint2.toBase58() 221 | ); 222 | 223 | // Deposit an NFT of collection mint 2 224 | holderNftTokenAccount = await getAssociatedTokenAddress( 225 | nftMint2, 226 | holderKeypair.publicKey 227 | ); 228 | solventNftTokenAccount = await getAssociatedTokenAddress( 229 | nftMint2, 230 | solventAuthorityAddress, 231 | true 232 | ); 233 | 234 | // Deposit NFT 2 into Solvent 235 | await provider.connection.confirmTransaction( 236 | await program.methods 237 | .depositNft(false, null) 238 | .accounts({ 239 | signer: holderKeypair.publicKey, 240 | dropletMint, 241 | nftMint: nftMint2, 242 | nftMetadata: nftMetadata2, 243 | signerNftTokenAccount: holderNftTokenAccount, 244 | solventNftTokenAccount, 245 | destinationDropletTokenAccount: holderDropletTokenAccount.address, 246 | }) 247 | .signers([holderKeypair]) 248 | .rpc() 249 | ); 250 | 251 | // Ensure user received another 100 droplets for the NFT deposited 252 | holderDropletTokenAccount = await getAccount( 253 | provider.connection, 254 | holderDropletTokenAccount.address 255 | ); 256 | expect(holderDropletTokenAccount.amount).to.equal(BigInt(200 * 100000000)); 257 | 258 | // Create a new collection mint: collection mint 3 259 | const { mint: collectionMint3 } = await mintNft( 260 | provider, 261 | nftSymbol, 262 | collectionCreatorKeypair, 263 | collectionCreatorKeypair.publicKey 264 | ); 265 | 266 | // Mint an NFT from collection mint 2 267 | const { mint: nftMint3 } = await mintNft( 268 | provider, 269 | nftSymbol, 270 | creatorKeypair, 271 | holderKeypair.publicKey, 272 | collectionMint3 273 | ); 274 | await verifyCollection( 275 | provider, 276 | nftMint3, 277 | collectionMint3, 278 | collectionCreatorKeypair 279 | ); 280 | 281 | // Update collection info to use collection mint 3 282 | await provider.connection.confirmTransaction( 283 | await program.methods 284 | // @ts-ignore 285 | .updateCollectionInfo({ v2: { collectionMint: collectionMint3 } }) 286 | .accounts({ 287 | signer: SOLVENT_ADMIN.publicKey, 288 | dropletMint: dropletMint, 289 | }) 290 | .signers([SOLVENT_ADMIN]) 291 | .rpc() 292 | ); 293 | 294 | // Redeem an NFT from the bucket 295 | holderNftTokenAccount = await getAssociatedTokenAddress( 296 | nftMint1, 297 | holderKeypair.publicKey 298 | ); 299 | solventNftTokenAccount = await getAssociatedTokenAddress( 300 | nftMint1, 301 | solventAuthorityAddress, 302 | true 303 | ); 304 | const solventTreasuryDropletTokenAccount = await getAssociatedTokenAddress( 305 | dropletMint, 306 | SOLVENT_CORE_TREASURY, 307 | true 308 | ); 309 | 310 | // Redeem NFT 311 | await provider.connection.confirmTransaction( 312 | await program.methods 313 | .redeemNft(false) 314 | .accounts({ 315 | signer: holderKeypair.publicKey, 316 | distributor: SOLVENT_CORE_TREASURY, 317 | distributorDropletTokenAccount: solventTreasuryDropletTokenAccount, 318 | dropletMint, 319 | nftMint: nftMint1, 320 | solventNftTokenAccount, 321 | solventTreasury: SOLVENT_CORE_TREASURY, 322 | solventTreasuryDropletTokenAccount, 323 | destinationNftTokenAccount: holderNftTokenAccount, 324 | signerDropletTokenAccount: holderDropletTokenAccount.address, 325 | }) 326 | .signers([holderKeypair]) 327 | .rpc() 328 | ); 329 | 330 | // Ensure user lost 100 droplets for the NFT redeemed 331 | holderDropletTokenAccount = await getAccount( 332 | provider.connection, 333 | holderDropletTokenAccount.address 334 | ); 335 | expect(holderDropletTokenAccount.amount).to.equal(BigInt(100 * 100000000)); 336 | }); 337 | 338 | it("fails to update collection info when signer is not Solvent admin", async () => { 339 | // Create a new collection NFT 340 | const collectionCreatorKeypair = await createKeypair(provider); 341 | const { mint: collectionMint } = await mintNft( 342 | provider, 343 | "DGOD", 344 | collectionCreatorKeypair, 345 | collectionCreatorKeypair.publicKey 346 | ); 347 | 348 | const maliciousActorKeypair = await createKeypair(provider); 349 | 350 | try { 351 | // Update collection info 352 | await program.methods 353 | // @ts-ignore 354 | .updateCollectionInfo({ v2: collectionMint }) 355 | .accounts({ 356 | signer: maliciousActorKeypair.publicKey, 357 | dropletMint: dropletMint, 358 | }) 359 | .signers([maliciousActorKeypair]) 360 | .rpc(); 361 | } catch (error) { 362 | expect(error.message).to.contain("You do not have administrator access."); 363 | return; 364 | } 365 | expect.fail( 366 | "Program did not fail to update bucket params when signer is not Solvent admin" 367 | ); 368 | }); 369 | 370 | afterEach(() => { 371 | dropletMint = undefined; 372 | bucketStateAddress = undefined; 373 | }); 374 | }); 375 | -------------------------------------------------------------------------------- /tests/tests/common.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { resolve } from "path"; 3 | import { readFileSync } from "fs"; 4 | import { SolventProtocol } from "../../target/types/solvent_protocol"; 5 | import * as gemFarmIdl from "../idls/gem_farm.json"; 6 | import * as gemBankIdl from "../idls/gem_bank.json"; 7 | import { GemBankClient, GemFarmClient } from "@gemworks/gem-farm-ts"; 8 | import { default as smbMintStrings } from "../nft-mints/smb.json"; 9 | 10 | export const BUCKET_SEED = Buffer.from("bucket-seed-v3"); 11 | export const SOLVENT_AUTHORITY_SEED = Buffer.from("authority-seed"); 12 | export const FARMER_AUTHORITY_SEED = Buffer.from("farmer-authority-seed"); 13 | export const LOCKER_SEED = Buffer.from("locker-seed"); 14 | export const DEPOSIT_SEED = Buffer.from("deposit-seed"); 15 | export const SOLVENT_CORE_TREASURY = new anchor.web3.PublicKey( 16 | "45nueWN9Qwn5vDBmJGBLEsYvaJG6vrNmNdCyrntXDk2K" 17 | ); 18 | export const SOLVENT_LOCKERS_TREASURY = new anchor.web3.PublicKey( 19 | "HkjFiwUW7qnREVm2PxBg8LUrCvjExrJjyYY51wsZTUK8" 20 | ); 21 | // TODO: To remove after migration is done 22 | export const SOLVENT_V1_PROGRAM_ID = new anchor.web3.PublicKey( 23 | "nft3agWJsaL1nN7pERYDFJUf54BzDZwS3oRbEzjrq6q" 24 | ); 25 | export const SOLVENT_ADMIN = anchor.web3.Keypair.fromSecretKey( 26 | Buffer.from( 27 | JSON.parse( 28 | readFileSync( 29 | resolve(__dirname, "../", "keypairs", "solvent-admin.json"), 30 | "utf-8" 31 | ) 32 | ) 33 | ) 34 | ); 35 | 36 | export const provider = anchor.getProvider() as anchor.AnchorProvider; 37 | 38 | export const program = anchor.workspace 39 | .SolventProtocol as anchor.Program; 40 | 41 | export const getGemFarm = ( 42 | gemFarmProgramId: anchor.web3.PublicKey, 43 | gemBankProgramId: anchor.web3.PublicKey 44 | ) => 45 | new GemFarmClient( 46 | provider.connection, 47 | provider.wallet as anchor.Wallet, 48 | // @ts-ignore 49 | gemFarmIdl, 50 | gemFarmProgramId, 51 | gemBankIdl, 52 | gemBankProgramId 53 | ); 54 | 55 | export const getGemBank = (gemBankProgramId: anchor.web3.PublicKey) => 56 | new GemBankClient( 57 | provider.connection, 58 | provider.wallet as anchor.Wallet, 59 | // @ts-ignore 60 | gemBankIdl, 61 | gemBankProgramId 62 | ); 63 | 64 | export interface NftInfo { 65 | nftMintAddress: anchor.web3.PublicKey; 66 | nftMetadataAddress: anchor.web3.PublicKey; 67 | holderKeypair: anchor.web3.Keypair; 68 | } 69 | 70 | export const smbMints = smbMintStrings.map((x) => new anchor.web3.PublicKey(x)); 71 | 72 | export const LAMPORTS_PER_DROPLET = 100000000n; 73 | -------------------------------------------------------------------------------- /tests/tests/deposit-nfts/redeem-nfts.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { 3 | getAccount, 4 | getAssociatedTokenAddress, 5 | getOrCreateAssociatedTokenAccount, 6 | } from "@solana/spl-token-latest"; 7 | import { expect } from "chai"; 8 | import { createKeypair, mintNft, verifyCollection } from "../../utils"; 9 | import { 10 | program, 11 | provider, 12 | SOLVENT_AUTHORITY_SEED, 13 | NftInfo, 14 | BUCKET_SEED, 15 | LAMPORTS_PER_DROPLET, 16 | SOLVENT_CORE_TREASURY as SOLVENT_TREASURY, 17 | SOLVENT_ADMIN, 18 | } from "../common"; 19 | 20 | describe("Redeeming NFTs from bucket", () => { 21 | const nftSymbol = "DAPE"; 22 | 23 | let solventAuthorityAddress: anchor.web3.PublicKey; 24 | 25 | before(async () => { 26 | [solventAuthorityAddress] = await anchor.web3.PublicKey.findProgramAddress( 27 | [SOLVENT_AUTHORITY_SEED], 28 | program.programId 29 | ); 30 | 31 | await provider.connection.confirmTransaction( 32 | await provider.connection.requestAirdrop( 33 | SOLVENT_ADMIN.publicKey, 34 | 10 * anchor.web3.LAMPORTS_PER_SOL 35 | ) 36 | ); 37 | }); 38 | 39 | let dropletMint: anchor.web3.PublicKey, 40 | bucketStateAddress: anchor.web3.PublicKey; 41 | // One of the verified creators 42 | let creatorKeypair: anchor.web3.Keypair; 43 | 44 | const nftInfos: NftInfo[] = []; 45 | 46 | beforeEach(async () => { 47 | // An NFT enthusiast wants to create a bucket for an NFT collection 48 | const bucketCreatorKeypair = await createKeypair(provider); 49 | 50 | // Create the bucket address 51 | const dropletMintKeypair = new anchor.web3.Keypair(); 52 | dropletMint = dropletMintKeypair.publicKey; 53 | [bucketStateAddress] = await anchor.web3.PublicKey.findProgramAddress( 54 | [dropletMint.toBuffer(), BUCKET_SEED], 55 | program.programId 56 | ); 57 | 58 | const collectionCreatorKeypair = await createKeypair(provider); 59 | const { mint: collectionMint } = await mintNft( 60 | provider, 61 | nftSymbol, 62 | collectionCreatorKeypair, 63 | collectionCreatorKeypair.publicKey 64 | ); 65 | 66 | const holderKeypair = await createKeypair(provider); 67 | 68 | // Minting 3 NFTs and sending them to a single user 69 | for (const i of Array(3)) { 70 | // Generate NFT creator and holder keypairs 71 | creatorKeypair = await createKeypair(provider); 72 | 73 | // Creator mints an NFT and sends it to holder 74 | const { mint, metadata } = await mintNft( 75 | provider, 76 | nftSymbol, 77 | creatorKeypair, 78 | holderKeypair.publicKey, 79 | collectionMint 80 | ); 81 | 82 | // Collection authority verifies that the NFT belongs to the collection 83 | await verifyCollection( 84 | provider, 85 | mint, 86 | collectionMint, 87 | collectionCreatorKeypair 88 | ); 89 | 90 | // Set public vars' values 91 | nftInfos.push({ 92 | nftMintAddress: mint, 93 | nftMetadataAddress: metadata, 94 | holderKeypair, 95 | }); 96 | } 97 | 98 | // Create bucket on Solvent 99 | await provider.connection.confirmTransaction( 100 | await program.methods 101 | // @ts-ignore 102 | .createBucket({ v2: { collectionMint } }) 103 | .accounts({ 104 | signer: bucketCreatorKeypair.publicKey, 105 | dropletMint: dropletMintKeypair.publicKey, 106 | }) 107 | .signers([dropletMintKeypair, bucketCreatorKeypair]) 108 | .rpc() 109 | ); 110 | 111 | // Looping through all the NFTs and depositing them in the bucket 112 | for (const { 113 | nftMintAddress, 114 | nftMetadataAddress, 115 | holderKeypair, 116 | } of nftInfos) { 117 | // NFT holder's NFT account 118 | let holderNftTokenAccount = await getOrCreateAssociatedTokenAccount( 119 | provider.connection, 120 | holderKeypair, 121 | nftMintAddress, 122 | holderKeypair.publicKey 123 | ); 124 | 125 | // Bucket's NFT account, a PDA owned by the Solvent program 126 | let solventNftTokenAccount = await getAssociatedTokenAddress( 127 | nftMintAddress, 128 | solventAuthorityAddress, 129 | true 130 | ); 131 | 132 | // The holder's droplet account 133 | let holderDropletTokenAccount = await getOrCreateAssociatedTokenAccount( 134 | provider.connection, 135 | holderKeypair, 136 | dropletMintKeypair.publicKey, 137 | holderKeypair.publicKey 138 | ); 139 | 140 | // Deposit NFT into Solvent 141 | await provider.connection.confirmTransaction( 142 | await program.methods 143 | .depositNft(false, null) 144 | .accounts({ 145 | signer: holderKeypair.publicKey, 146 | dropletMint: dropletMintKeypair.publicKey, 147 | nftMint: nftMintAddress, 148 | nftMetadata: nftMetadataAddress, 149 | signerNftTokenAccount: holderNftTokenAccount.address, 150 | solventNftTokenAccount, 151 | destinationDropletTokenAccount: holderDropletTokenAccount.address, 152 | }) 153 | .signers([holderKeypair]) 154 | .rpc() 155 | ); 156 | } 157 | }); 158 | 159 | it("can redeem NFTs from bucket", async () => { 160 | // Looping through all the NFTs and redeeming them from the bucket 161 | for (const { nftMintAddress, holderKeypair } of nftInfos.slice(0, -1)) { 162 | // NFT holder's NFT account 163 | let holderNftTokenAccount = await getOrCreateAssociatedTokenAccount( 164 | provider.connection, 165 | holderKeypair, 166 | nftMintAddress, 167 | holderKeypair.publicKey 168 | ); 169 | 170 | // Solvent's NFT account, a PDA owned by the Solvent program 171 | let solventNftTokenAccount = await getAssociatedTokenAddress( 172 | nftMintAddress, 173 | solventAuthorityAddress, 174 | true 175 | ); 176 | 177 | // The holder's droplet account 178 | const holderDropletTokenAccount = await getOrCreateAssociatedTokenAccount( 179 | provider.connection, 180 | holderKeypair, 181 | dropletMint, 182 | holderKeypair.publicKey 183 | ); 184 | 185 | const solventTreasuryDropletTokenAccount = 186 | await getAssociatedTokenAddress(dropletMint, SOLVENT_TREASURY, true); 187 | 188 | let bucketState = await program.account.bucketStateV3.fetch( 189 | bucketStateAddress 190 | ); 191 | const numNftsInBucket = bucketState.numNftsInBucket; 192 | 193 | // Redeem NFT 194 | await provider.connection.confirmTransaction( 195 | await program.methods 196 | .redeemNft(false) 197 | .accounts({ 198 | signer: holderKeypair.publicKey, 199 | distributor: SOLVENT_TREASURY, 200 | distributorDropletTokenAccount: solventTreasuryDropletTokenAccount, 201 | dropletMint, 202 | nftMint: nftMintAddress, 203 | solventNftTokenAccount, 204 | solventTreasury: SOLVENT_TREASURY, 205 | solventTreasuryDropletTokenAccount, 206 | destinationNftTokenAccount: holderNftTokenAccount.address, 207 | signerDropletTokenAccount: holderDropletTokenAccount.address, 208 | }) 209 | .signers([holderKeypair]) 210 | .rpc() 211 | ); 212 | 213 | // Ensure user burned 100 droplets 214 | expect( 215 | holderDropletTokenAccount.amount - 216 | ( 217 | await getAccount( 218 | provider.connection, 219 | holderDropletTokenAccount.address 220 | ) 221 | ).amount 222 | ).to.equal(100n * LAMPORTS_PER_DROPLET); 223 | 224 | // Ensure user received 1 NFT 225 | holderNftTokenAccount = await getOrCreateAssociatedTokenAccount( 226 | provider.connection, 227 | holderKeypair, 228 | nftMintAddress, 229 | holderKeypair.publicKey 230 | ); 231 | expect(holderNftTokenAccount.amount).to.equal(1n); 232 | 233 | // Ensure counter decreases in bucket 234 | bucketState = await program.account.bucketStateV3.fetch( 235 | bucketStateAddress 236 | ); 237 | expect(bucketState.numNftsInBucket).to.equal(numNftsInBucket - 1); 238 | } 239 | }); 240 | 241 | afterEach(() => { 242 | dropletMint = undefined; 243 | bucketStateAddress = undefined; 244 | creatorKeypair = undefined; 245 | nftInfos.length = 0; 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /tests/tests/lock-nfts/update-locking-params.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { createKeypair, mintNft } from "../../utils"; 3 | import { program, provider, BUCKET_SEED, SOLVENT_ADMIN } from "../common"; 4 | import { expect } from "chai"; 5 | import { beforeEach } from "mocha"; 6 | 7 | describe("Updating locking related params", () => { 8 | const nftSymbol = "DAPE"; 9 | 10 | let dropletMint: anchor.web3.PublicKey, 11 | bucketStateAddress: anchor.web3.PublicKey; 12 | 13 | beforeEach(async () => { 14 | // An NFT enthusiast wants to create a bucket for an NFT collection 15 | const userKeypair = await createKeypair(provider); 16 | 17 | // Create the bucket address 18 | const dropletMintKeypair = new anchor.web3.Keypair(); 19 | dropletMint = dropletMintKeypair.publicKey; 20 | 21 | [bucketStateAddress] = await anchor.web3.PublicKey.findProgramAddress( 22 | [dropletMintKeypair.publicKey.toBuffer(), BUCKET_SEED], 23 | program.programId 24 | ); 25 | 26 | // Create the collection NFT 27 | const collectionCreatorKeypair = await createKeypair(provider); 28 | const { mint: collectionMint } = await mintNft( 29 | provider, 30 | nftSymbol, 31 | collectionCreatorKeypair, 32 | collectionCreatorKeypair.publicKey 33 | ); 34 | 35 | // Create bucket on Solvent 36 | await provider.connection.confirmTransaction( 37 | await program.methods 38 | // @ts-ignore 39 | .createBucket({ v2: { collectionMint } }) 40 | .accounts({ 41 | signer: userKeypair.publicKey, 42 | dropletMint: dropletMint, 43 | }) 44 | .signers([dropletMintKeypair, userKeypair]) 45 | .rpc() 46 | ); 47 | }); 48 | 49 | it("can update locking params", async () => { 50 | // Update bucket params 51 | await provider.connection.confirmTransaction( 52 | await program.methods 53 | .updateLockingParams(new anchor.BN(10_000), 100) 54 | .accounts({ 55 | signer: SOLVENT_ADMIN.publicKey, 56 | dropletMint: dropletMint, 57 | }) 58 | .signers([SOLVENT_ADMIN]) 59 | .rpc() 60 | ); 61 | 62 | // Fetch BucketState account and assert it has correct data 63 | const bucketState = await program.account.bucketStateV3.fetch( 64 | bucketStateAddress 65 | ); 66 | expect(bucketState.maxLockerDuration.toNumber()).to.equal(10_000); 67 | expect(bucketState.interestScaler).to.equal(100); 68 | }); 69 | 70 | it("fails to update locking params when signer is not Solvent admin", async () => { 71 | const maliciousActorKeypair = await createKeypair(provider); 72 | try { 73 | // Update bucket params 74 | await program.methods 75 | .updateLockingParams(new anchor.BN(10_000), 100) 76 | .accounts({ 77 | signer: maliciousActorKeypair.publicKey, 78 | dropletMint: dropletMint, 79 | }) 80 | .signers([maliciousActorKeypair]) 81 | .rpc(); 82 | } catch (error) { 83 | expect(error.message).to.contain("You do not have administrator access."); 84 | return; 85 | } 86 | expect.fail( 87 | "Program did not fail to update bucket params when signer is not Solvent admin" 88 | ); 89 | }); 90 | 91 | it("fails to update locking params when interest scaler is too large", async () => { 92 | try { 93 | // Update bucket params 94 | await program.methods 95 | .updateLockingParams(new anchor.BN(10_000), 150) 96 | .accounts({ 97 | signer: SOLVENT_ADMIN.publicKey, 98 | dropletMint: dropletMint, 99 | }) 100 | .signers([SOLVENT_ADMIN]) 101 | .rpc(); 102 | } catch (error) { 103 | expect(error.message).to.contain( 104 | "Interest scaler entered by you is larger than the max value." 105 | ); 106 | return; 107 | } 108 | expect.fail( 109 | "Program did not fail to update bucket params when given interest scaler is too large" 110 | ); 111 | }); 112 | 113 | afterEach(() => { 114 | dropletMint = undefined; 115 | bucketStateAddress = undefined; 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /tests/tests/migration/migrate-droplets.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { createKeypair, getMerkleTree, mintNft } from "../../utils"; 3 | import { 4 | SOLVENT_AUTHORITY_SEED, 5 | SOLVENT_ADMIN, 6 | SOLVENT_V1_PROGRAM_ID, 7 | NftInfo, 8 | program, 9 | provider, 10 | SOLVENT_LOCKERS_TREASURY as SOLVENT_TREASURY, 11 | smbMints, 12 | } from "../common"; 13 | import { 14 | createAssociatedTokenAccount, 15 | createMint, 16 | getAccount, 17 | getAssociatedTokenAddress, 18 | getOrCreateAssociatedTokenAccount, 19 | mintToChecked, 20 | } from "@solana/spl-token-latest"; 21 | import { Solvent } from "../../types/solvent"; 22 | import * as solventIdl from "../../idls/solvent.json"; 23 | import { expect } from "chai"; 24 | 25 | describe("Migrating droplets from Solvent v1 to v2", () => { 26 | const nftSymbol = "DAPE"; 27 | 28 | const solventV1 = new anchor.Program( 29 | // @ts-ignore 30 | solventIdl, 31 | SOLVENT_V1_PROGRAM_ID, 32 | provider 33 | ) as anchor.Program; 34 | 35 | let solventAuthorityAddressOld: anchor.web3.PublicKey, 36 | solventAuthorityBumpOld: number; 37 | 38 | const { root: whitelistRoot } = getMerkleTree(smbMints); 39 | 40 | before(async () => { 41 | await provider.connection.confirmTransaction( 42 | await provider.connection.requestAirdrop( 43 | SOLVENT_ADMIN.publicKey, 44 | 10 * anchor.web3.LAMPORTS_PER_SOL 45 | ) 46 | ); 47 | 48 | // Generate a PDA of the Solvent program which holds authority over the bucket 49 | [solventAuthorityAddressOld, solventAuthorityBumpOld] = 50 | await anchor.web3.PublicKey.findProgramAddress( 51 | [SOLVENT_AUTHORITY_SEED], 52 | solventV1.programId 53 | ); 54 | }); 55 | 56 | let dropletMintOld: anchor.web3.PublicKey, 57 | dropletMintNew: anchor.web3.PublicKey; 58 | const nftInfos: NftInfo[] = []; 59 | 60 | beforeEach(async () => { 61 | // Addresses of verified creators of the NFT collection 62 | const creatorAddresses: anchor.web3.PublicKey[] = []; 63 | 64 | // Minting 3 NFTs and sending them to 3 different users 65 | for (const i of Array(3)) { 66 | // Generate NFT creator and holder keypairs 67 | const holderKeypair = await createKeypair(provider); 68 | const creatorKeypair = await createKeypair(provider); 69 | 70 | // Creator mints an NFT and sends it to holder 71 | const { mint, metadata } = await mintNft( 72 | provider, 73 | nftSymbol, 74 | creatorKeypair, 75 | holderKeypair.publicKey 76 | ); 77 | 78 | // Set public vars' values 79 | nftInfos.push({ 80 | nftMintAddress: mint, 81 | nftMetadataAddress: metadata, 82 | holderKeypair, 83 | }); 84 | creatorAddresses.push(creatorKeypair.publicKey); 85 | } 86 | 87 | // Create bucket on Solvent V1 88 | const dropletMintKeypairOld = new anchor.web3.Keypair(); 89 | dropletMintOld = dropletMintKeypairOld.publicKey; 90 | const [bucketStateAddressOld, bucketStateBumpOld] = 91 | await anchor.web3.PublicKey.findProgramAddress( 92 | [dropletMintOld.toBuffer(), Buffer.from("bucket-seed-v2")], 93 | solventV1.programId 94 | ); 95 | 96 | const tx = new anchor.web3.Transaction(); 97 | tx.add( 98 | await solventV1.methods 99 | .newBucketV2(solventAuthorityBumpOld, bucketStateBumpOld, nftSymbol) 100 | .accounts({ 101 | bucketCreator: provider.wallet.publicKey, 102 | bucketMint: dropletMintOld, 103 | bucketStateV2: bucketStateAddressOld, 104 | solventAuthority: solventAuthorityAddressOld, 105 | }) 106 | .signers([dropletMintKeypairOld]) 107 | .remainingAccounts( 108 | creatorAddresses.map((value) => { 109 | return { 110 | pubkey: value, 111 | isSigner: false, 112 | isWritable: true, 113 | }; 114 | }) 115 | ) 116 | .instruction() 117 | ); 118 | 119 | try { 120 | await provider.sendAndConfirm(tx, [dropletMintKeypairOld]); 121 | } catch (error) { 122 | console.log(error); 123 | throw error; 124 | } 125 | 126 | // Deposit all NFTs into the old Solvent's bucket 127 | for (const { 128 | nftMintAddress, 129 | nftMetadataAddress, 130 | holderKeypair, 131 | } of nftInfos) { 132 | // NFT holder's NFT account 133 | const holderNftTokenAccount = await getOrCreateAssociatedTokenAccount( 134 | provider.connection, 135 | holderKeypair, 136 | nftMintAddress, 137 | holderKeypair.publicKey 138 | ); 139 | 140 | // Bucket's NFT account, a PDA owned by the Solvent program 141 | const solventNftTokenAccount = await getOrCreateAssociatedTokenAccount( 142 | provider.connection, 143 | holderKeypair, 144 | nftMintAddress, 145 | solventAuthorityAddressOld, 146 | true 147 | ); 148 | 149 | // The holder's droplet account 150 | let holderDropletTokenAccount = await getOrCreateAssociatedTokenAccount( 151 | provider.connection, 152 | holderKeypair, 153 | dropletMintOld, 154 | holderKeypair.publicKey 155 | ); 156 | 157 | const solventTreasuryDropletAccount = 158 | await getOrCreateAssociatedTokenAccount( 159 | provider.connection, 160 | holderKeypair, 161 | dropletMintOld, 162 | SOLVENT_TREASURY 163 | ); 164 | 165 | // Deposit NFT into Solvent 166 | await provider.connection.confirmTransaction( 167 | await solventV1.methods 168 | .mintDropletV2(solventAuthorityBumpOld) 169 | .accounts({ 170 | signerWallet: holderKeypair.publicKey, 171 | solventAuthority: solventAuthorityAddressOld, 172 | bucketMint: dropletMintOld, 173 | bucketStateV2: bucketStateAddressOld, 174 | nftMint: nftMintAddress, 175 | metadata: nftMetadataAddress, 176 | signerTokenAc: holderNftTokenAccount.address, 177 | solventTokenAc: solventNftTokenAccount.address, 178 | solventMintFeeAc: solventTreasuryDropletAccount.address, 179 | destinationDropletAc: holderDropletTokenAccount.address, 180 | }) 181 | .signers([holderKeypair]) 182 | .rpc() 183 | ); 184 | } 185 | 186 | // Create bucket on Solvent V2 187 | const bucketCreatorKeypair = await createKeypair(provider); 188 | const dropletMintKeypairNew = new anchor.web3.Keypair(); 189 | dropletMintNew = dropletMintKeypairNew.publicKey; 190 | 191 | await provider.connection.confirmTransaction( 192 | await program.methods 193 | // @ts-ignore 194 | .createBucket({ 195 | v1: { 196 | verifiedCreators: creatorAddresses, 197 | symbol: nftSymbol, 198 | whitelistRoot, 199 | }, 200 | }) 201 | .accounts({ 202 | signer: bucketCreatorKeypair.publicKey, 203 | dropletMint: dropletMintNew, 204 | }) 205 | .signers([dropletMintKeypairNew, bucketCreatorKeypair]) 206 | .rpc() 207 | ); 208 | 209 | // Start migration 210 | await provider.connection.confirmTransaction( 211 | await program.methods 212 | .startMigration() 213 | .accounts({ 214 | signer: SOLVENT_ADMIN.publicKey, 215 | dropletMintOld, 216 | dropletMintNew, 217 | }) 218 | .signers([SOLVENT_ADMIN]) 219 | .rpc() 220 | ); 221 | }); 222 | 223 | it("can migrate droplets", async () => { 224 | // Deposit all NFTs into the old Solvent's bucket 225 | for (const [index, { holderKeypair }] of nftInfos.entries()) { 226 | // The holder's droplet accounts 227 | const holderDropletTokenAccountOld = 228 | await getOrCreateAssociatedTokenAccount( 229 | provider.connection, 230 | holderKeypair, 231 | dropletMintOld, 232 | holderKeypair.publicKey 233 | ); 234 | 235 | const holderDropletTokenAccountNew = 236 | await getOrCreateAssociatedTokenAccount( 237 | provider.connection, 238 | holderKeypair, 239 | dropletMintNew, 240 | holderKeypair.publicKey 241 | ); 242 | 243 | const solventMigrationCrankDropletTokenAccount = 244 | await getAssociatedTokenAddress( 245 | dropletMintOld, 246 | SOLVENT_ADMIN.publicKey, 247 | true 248 | ); 249 | 250 | // Migrate droplets 251 | await provider.connection.confirmTransaction( 252 | await program.methods 253 | .migrateDroplets() 254 | .accounts({ 255 | signer: holderKeypair.publicKey, 256 | solventMigrationCrank: SOLVENT_ADMIN.publicKey, 257 | dropletMintOld, 258 | dropletMintNew, 259 | signerDropletTokenAccountOld: holderDropletTokenAccountOld.address, 260 | signerDropletTokenAccountNew: holderDropletTokenAccountNew.address, 261 | solventMigrationCrankDropletTokenAccount, 262 | }) 263 | .signers([holderKeypair]) 264 | .rpc() 265 | ); 266 | 267 | // Assert Solvent received the droplets 268 | expect( 269 | ( 270 | await getAccount( 271 | provider.connection, 272 | solventMigrationCrankDropletTokenAccount 273 | ) 274 | ).amount 275 | ).to.equal(10000000000n * BigInt(index + 1)); 276 | 277 | // Assert user lost v1 droplets 278 | expect( 279 | ( 280 | await getAccount( 281 | provider.connection, 282 | holderDropletTokenAccountOld.address 283 | ) 284 | ).amount 285 | ).to.equal(0n); 286 | 287 | // Assert user received new droplets 288 | expect( 289 | ( 290 | await getAccount( 291 | provider.connection, 292 | holderDropletTokenAccountNew.address 293 | ) 294 | ).amount 295 | ).to.equal(10000000000n); 296 | } 297 | }); 298 | 299 | it("fails to migrate droplets when given invalid old droplets", async () => { 300 | const maliciousActorKeypair = await createKeypair(provider); 301 | 302 | const invalidDropletMint = await createMint( 303 | provider.connection, 304 | maliciousActorKeypair, 305 | maliciousActorKeypair.publicKey, 306 | null, 307 | 8 308 | ); 309 | 310 | const maliciousActorDropletTokenAccountOld = 311 | await createAssociatedTokenAccount( 312 | provider.connection, 313 | maliciousActorKeypair, 314 | invalidDropletMint, 315 | maliciousActorKeypair.publicKey 316 | ); 317 | 318 | await mintToChecked( 319 | provider.connection, 320 | maliciousActorKeypair, 321 | invalidDropletMint, 322 | maliciousActorDropletTokenAccountOld, 323 | maliciousActorKeypair.publicKey, 324 | 100, 325 | 8 326 | ); 327 | 328 | const maliciousActorDropletTokenAccountNew = 329 | await getOrCreateAssociatedTokenAccount( 330 | provider.connection, 331 | maliciousActorKeypair, 332 | dropletMintNew, 333 | maliciousActorKeypair.publicKey 334 | ); 335 | 336 | const solventMigrationCrankDropletTokenAccount = 337 | await getAssociatedTokenAddress( 338 | invalidDropletMint, 339 | SOLVENT_ADMIN.publicKey, 340 | true 341 | ); 342 | 343 | // Migrate droplets 344 | try { 345 | await provider.connection.confirmTransaction( 346 | await program.methods 347 | .migrateDroplets() 348 | .accounts({ 349 | signer: maliciousActorKeypair.publicKey, 350 | solventMigrationCrank: SOLVENT_ADMIN.publicKey, 351 | dropletMintOld: invalidDropletMint, 352 | dropletMintNew, 353 | signerDropletTokenAccountOld: maliciousActorDropletTokenAccountOld, 354 | signerDropletTokenAccountNew: 355 | maliciousActorDropletTokenAccountNew.address, 356 | solventMigrationCrankDropletTokenAccount, 357 | }) 358 | .signers([maliciousActorKeypair]) 359 | .rpc() 360 | ); 361 | } catch (error) { 362 | expect(error.message).contains("A has one constraint was violated."); 363 | return; 364 | } 365 | expect.fail( 366 | "Program did not fail while migrating droplets with invalid older droplet." 367 | ); 368 | }); 369 | 370 | afterEach(async () => { 371 | dropletMintOld = undefined; 372 | dropletMintNew = undefined; 373 | nftInfos.length = 0; 374 | }); 375 | }); 376 | -------------------------------------------------------------------------------- /tests/tests/migration/migrate-nft.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { 3 | getAccount, 4 | getAssociatedTokenAddress, 5 | getOrCreateAssociatedTokenAccount, 6 | transfer, 7 | } from "@solana/spl-token-latest"; 8 | import { expect } from "chai"; 9 | import { createKeypair, mintNft, verifyCollection } from "../../utils"; 10 | import { 11 | program, 12 | provider, 13 | SOLVENT_AUTHORITY_SEED, 14 | NftInfo, 15 | BUCKET_SEED, 16 | SOLVENT_ADMIN, 17 | } from "../common"; 18 | 19 | describe("Migrating NFTs into bucket", () => { 20 | const nftSymbol = "DAPE"; 21 | 22 | let solventAuthorityAddress: anchor.web3.PublicKey; 23 | 24 | before(async () => { 25 | [solventAuthorityAddress] = await anchor.web3.PublicKey.findProgramAddress( 26 | [SOLVENT_AUTHORITY_SEED], 27 | program.programId 28 | ); 29 | 30 | await provider.connection.confirmTransaction( 31 | await provider.connection.requestAirdrop( 32 | SOLVENT_ADMIN.publicKey, 33 | 10 * anchor.web3.LAMPORTS_PER_SOL 34 | ) 35 | ); 36 | }); 37 | 38 | // One of the verified creators 39 | let creatorKeypair: anchor.web3.Keypair; 40 | 41 | const nftInfos: NftInfo[] = []; 42 | let dropletMint: anchor.web3.PublicKey, 43 | bucketStateAddress: anchor.web3.PublicKey; 44 | 45 | beforeEach(async () => { 46 | const collectionCreatorKeypair = await createKeypair(provider); 47 | const { mint: collectionMint } = await mintNft( 48 | provider, 49 | nftSymbol, 50 | collectionCreatorKeypair, 51 | collectionCreatorKeypair.publicKey 52 | ); 53 | 54 | // Minting 3 NFTs and sending them to Solvent admin 55 | for (const i of Array(3)) { 56 | // Generate NFT creator keypairs 57 | creatorKeypair = await createKeypair(provider); 58 | 59 | // Creator mints an NFT and sends it to holder 60 | const { mint, metadata } = await mintNft( 61 | provider, 62 | nftSymbol, 63 | creatorKeypair, 64 | SOLVENT_ADMIN.publicKey, 65 | collectionMint 66 | ); 67 | 68 | // Collection authority verifies that the NFT belongs to the collection 69 | await verifyCollection( 70 | provider, 71 | mint, 72 | collectionMint, 73 | collectionCreatorKeypair 74 | ); 75 | 76 | // Set public vars' values 77 | nftInfos.push({ 78 | nftMintAddress: mint, 79 | nftMetadataAddress: metadata, 80 | holderKeypair: SOLVENT_ADMIN, 81 | }); 82 | } 83 | 84 | // An NFT enthusiast wants to create a bucket for an NFT collection 85 | const bucketCreatorKeypair = await createKeypair(provider); 86 | 87 | // Create the bucket address 88 | const dropletMintKeypair = new anchor.web3.Keypair(); 89 | dropletMint = dropletMintKeypair.publicKey; 90 | [bucketStateAddress] = await anchor.web3.PublicKey.findProgramAddress( 91 | [dropletMint.toBuffer(), BUCKET_SEED], 92 | program.programId 93 | ); 94 | 95 | // Create bucket on Solvent 96 | await provider.connection.confirmTransaction( 97 | await program.methods 98 | // @ts-ignore 99 | .createBucket({ v2: { collectionMint } }) 100 | .accounts({ 101 | signer: bucketCreatorKeypair.publicKey, 102 | dropletMint: dropletMintKeypair.publicKey, 103 | }) 104 | .signers([dropletMintKeypair, bucketCreatorKeypair]) 105 | .rpc() 106 | ); 107 | }); 108 | 109 | it("can migrate NFTs into bucket", async () => { 110 | // Looping through all the NFTs and depositing them in the bucket 111 | for (const { nftMintAddress, nftMetadataAddress } of nftInfos) { 112 | let adminNftTokenAccount = await getOrCreateAssociatedTokenAccount( 113 | provider.connection, 114 | SOLVENT_ADMIN, 115 | nftMintAddress, 116 | SOLVENT_ADMIN.publicKey 117 | ); 118 | 119 | // Bucket's NFT account, a PDA owned by the Solvent program 120 | const solventNftTokenAccount = await getAssociatedTokenAddress( 121 | nftMintAddress, 122 | solventAuthorityAddress, 123 | true 124 | ); 125 | 126 | let bucketState = await program.account.bucketStateV3.fetch( 127 | bucketStateAddress 128 | ); 129 | const numNftsInBucket = bucketState.numNftsInBucket; 130 | 131 | // Deposit NFT into Solvent 132 | await provider.connection.confirmTransaction( 133 | await program.methods 134 | .migrateNft(null) 135 | .accounts({ 136 | signer: SOLVENT_ADMIN.publicKey, 137 | dropletMint, 138 | nftMint: nftMintAddress, 139 | nftMetadata: nftMetadataAddress, 140 | signerNftTokenAccount: adminNftTokenAccount.address, 141 | solventNftTokenAccount, 142 | }) 143 | .signers([SOLVENT_ADMIN]) 144 | .rpc() 145 | ); 146 | 147 | // Ensure user did not receive droplets for the NFT deposited 148 | const adminDropletTokenAccount = await getOrCreateAssociatedTokenAccount( 149 | provider.connection, 150 | SOLVENT_ADMIN, 151 | dropletMint, 152 | SOLVENT_ADMIN.publicKey 153 | ); 154 | expect( 155 | ( 156 | await getAccount( 157 | provider.connection, 158 | adminDropletTokenAccount.address 159 | ) 160 | ).amount 161 | ).to.equal(0n); 162 | 163 | // Ensure user does not have the deposited NFT 164 | expect( 165 | (await getAccount(provider.connection, adminNftTokenAccount.address)) 166 | .amount 167 | ).to.equal(0n); 168 | 169 | // Ensure Solvent received the deposited NFT 170 | const solventNftTokenAccountInfo = await getAccount( 171 | provider.connection, 172 | solventNftTokenAccount 173 | ); 174 | expect(solventNftTokenAccountInfo.amount).to.equal(1n); 175 | 176 | // Ensure counter increases in bucket 177 | bucketState = await program.account.bucketStateV3.fetch( 178 | bucketStateAddress 179 | ); 180 | expect(bucketState.numNftsInBucket).to.equal(numNftsInBucket + 1); 181 | } 182 | }); 183 | 184 | it("fails to migrate NFT when signer is not Solvent admin", async () => { 185 | // Looping through all the NFTs and depositing them in the bucket 186 | const { nftMintAddress, nftMetadataAddress } = nftInfos[0]; 187 | 188 | // Transfer the NFT to a random user 189 | const randomKeypair = await createKeypair(provider); 190 | let userNftTokenAccount = ( 191 | await getOrCreateAssociatedTokenAccount( 192 | provider.connection, 193 | randomKeypair, 194 | nftMintAddress, 195 | randomKeypair.publicKey 196 | ) 197 | ).address; 198 | 199 | await transfer( 200 | provider.connection, 201 | randomKeypair, 202 | await getAssociatedTokenAddress(nftMintAddress, SOLVENT_ADMIN.publicKey), 203 | userNftTokenAccount, 204 | SOLVENT_ADMIN, 205 | 1 206 | ); 207 | 208 | // Bucket's NFT account, a PDA owned by the Solvent program 209 | const solventNftTokenAccount = await getAssociatedTokenAddress( 210 | nftMintAddress, 211 | solventAuthorityAddress, 212 | true 213 | ); 214 | 215 | // Deposit NFT into Solvent 216 | try { 217 | await program.methods 218 | .migrateNft(null) 219 | .accounts({ 220 | signer: randomKeypair.publicKey, 221 | dropletMint, 222 | nftMint: nftMintAddress, 223 | nftMetadata: nftMetadataAddress, 224 | signerNftTokenAccount: userNftTokenAccount, 225 | solventNftTokenAccount, 226 | }) 227 | .signers([randomKeypair]) 228 | .rpc(); 229 | } catch (error) { 230 | expect(error.message).to.contain("You do not have administrator access."); 231 | return; 232 | } 233 | expect.fail( 234 | "Program did not fail while migrating NFT when signer is not Solvent admin" 235 | ); 236 | }); 237 | 238 | afterEach(() => { 239 | creatorKeypair = undefined; 240 | nftInfos.length = 0; 241 | dropletMint = undefined; 242 | bucketStateAddress = undefined; 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /tests/tests/migration/start-migration.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { createKeypair, getMerkleTree, mintNft } from "../../utils"; 3 | import { 4 | SOLVENT_AUTHORITY_SEED, 5 | SOLVENT_ADMIN, 6 | SOLVENT_V1_PROGRAM_ID, 7 | NftInfo, 8 | program, 9 | provider, 10 | SOLVENT_LOCKERS_TREASURY as SOLVENT_TREASURY, 11 | smbMints, 12 | } from "../common"; 13 | import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token-latest"; 14 | import { Solvent } from "../../types/solvent"; 15 | import * as solventIdl from "../../idls/solvent.json"; 16 | import { expect } from "chai"; 17 | 18 | // TODO: To remove after migration is done 19 | describe("Starting migration from Solvent v1 to v2", () => { 20 | const nftSymbol = "DAPE"; 21 | 22 | const solventV1 = new anchor.Program( 23 | // @ts-ignore 24 | solventIdl, 25 | SOLVENT_V1_PROGRAM_ID, 26 | provider 27 | ) as anchor.Program; 28 | 29 | let solventAuthorityAddressOld: anchor.web3.PublicKey, 30 | solventAuthorityBumpOld: number; 31 | 32 | const { root: whitelistRoot } = getMerkleTree(smbMints); 33 | 34 | before(async () => { 35 | await provider.connection.confirmTransaction( 36 | await provider.connection.requestAirdrop( 37 | SOLVENT_ADMIN.publicKey, 38 | 10 * anchor.web3.LAMPORTS_PER_SOL 39 | ) 40 | ); 41 | 42 | // Generate a PDA of the Solvent program which holds authority over the bucket 43 | [solventAuthorityAddressOld, solventAuthorityBumpOld] = 44 | await anchor.web3.PublicKey.findProgramAddress( 45 | [SOLVENT_AUTHORITY_SEED], 46 | solventV1.programId 47 | ); 48 | }); 49 | 50 | let dropletMintOld: anchor.web3.PublicKey, 51 | dropletMintNew: anchor.web3.PublicKey; 52 | 53 | beforeEach(async () => { 54 | // Addresses of verified creators of the NFT collection 55 | const creatorAddresses: anchor.web3.PublicKey[] = []; 56 | const nftInfos: NftInfo[] = []; 57 | 58 | // Minting 3 NFTs and sending them to 3 different users 59 | for (const i of Array(3)) { 60 | // Generate NFT creator and holder keypairs 61 | const holderKeypair = await createKeypair(provider); 62 | const creatorKeypair = await createKeypair(provider); 63 | 64 | // Creator mints an NFT and sends it to holder 65 | const { mint, metadata } = await mintNft( 66 | provider, 67 | nftSymbol, 68 | creatorKeypair, 69 | holderKeypair.publicKey 70 | ); 71 | 72 | // Set public vars' values 73 | nftInfos.push({ 74 | nftMintAddress: mint, 75 | nftMetadataAddress: metadata, 76 | holderKeypair, 77 | }); 78 | creatorAddresses.push(creatorKeypair.publicKey); 79 | } 80 | 81 | // Create bucket on Solvent V1 82 | const dropletMintKeypairOld = new anchor.web3.Keypair(); 83 | dropletMintOld = dropletMintKeypairOld.publicKey; 84 | const [bucketStateAddressOld, bucketStateBumpOld] = 85 | await anchor.web3.PublicKey.findProgramAddress( 86 | [dropletMintOld.toBuffer(), Buffer.from("bucket-seed-v2")], 87 | solventV1.programId 88 | ); 89 | 90 | const tx = new anchor.web3.Transaction(); 91 | tx.add( 92 | await solventV1.methods 93 | .newBucketV2(solventAuthorityBumpOld, bucketStateBumpOld, nftSymbol) 94 | .accounts({ 95 | bucketCreator: provider.wallet.publicKey, 96 | bucketMint: dropletMintOld, 97 | bucketStateV2: bucketStateAddressOld, 98 | solventAuthority: solventAuthorityAddressOld, 99 | }) 100 | .signers([dropletMintKeypairOld]) 101 | .remainingAccounts( 102 | creatorAddresses.map((value) => { 103 | return { 104 | pubkey: value, 105 | isSigner: false, 106 | isWritable: true, 107 | }; 108 | }) 109 | ) 110 | .instruction() 111 | ); 112 | 113 | try { 114 | await provider.sendAndConfirm(tx, [dropletMintKeypairOld]); 115 | } catch (error) { 116 | console.log(error); 117 | throw error; 118 | } 119 | 120 | // Deposit all NFTs into the old Solvent's bucket 121 | for (const { 122 | nftMintAddress, 123 | nftMetadataAddress, 124 | holderKeypair, 125 | } of nftInfos) { 126 | // NFT holder's NFT account 127 | const holderNftTokenAccount = await getOrCreateAssociatedTokenAccount( 128 | provider.connection, 129 | holderKeypair, 130 | nftMintAddress, 131 | holderKeypair.publicKey 132 | ); 133 | 134 | // Bucket's NFT account, a PDA owned by the Solvent program 135 | const solventNftTokenAccount = await getOrCreateAssociatedTokenAccount( 136 | provider.connection, 137 | holderKeypair, 138 | nftMintAddress, 139 | solventAuthorityAddressOld, 140 | true 141 | ); 142 | 143 | // The holder's droplet account 144 | let holderDropletTokenAccount = await getOrCreateAssociatedTokenAccount( 145 | provider.connection, 146 | holderKeypair, 147 | dropletMintOld, 148 | holderKeypair.publicKey 149 | ); 150 | 151 | const solventTreasuryDropletAccount = 152 | await getOrCreateAssociatedTokenAccount( 153 | provider.connection, 154 | holderKeypair, 155 | dropletMintOld, 156 | SOLVENT_TREASURY 157 | ); 158 | 159 | // Deposit NFT into Solvent 160 | await provider.connection.confirmTransaction( 161 | await solventV1.methods 162 | .mintDropletV2(solventAuthorityBumpOld) 163 | .accounts({ 164 | signerWallet: holderKeypair.publicKey, 165 | solventAuthority: solventAuthorityAddressOld, 166 | bucketMint: dropletMintOld, 167 | bucketStateV2: bucketStateAddressOld, 168 | nftMint: nftMintAddress, 169 | metadata: nftMetadataAddress, 170 | signerTokenAc: holderNftTokenAccount.address, 171 | solventTokenAc: solventNftTokenAccount.address, 172 | solventMintFeeAc: solventTreasuryDropletAccount.address, 173 | destinationDropletAc: holderDropletTokenAccount.address, 174 | }) 175 | .signers([holderKeypair]) 176 | .rpc() 177 | ); 178 | } 179 | 180 | // Create bucket on Solvent V2 181 | const bucketCreatorKeypair = await createKeypair(provider); 182 | const dropletMintKeypairNew = new anchor.web3.Keypair(); 183 | dropletMintNew = dropletMintKeypairNew.publicKey; 184 | 185 | await provider.connection.confirmTransaction( 186 | await program.methods 187 | // @ts-ignore 188 | .createBucket({ 189 | v1: { 190 | verifiedCreators: creatorAddresses, 191 | symbol: nftSymbol, 192 | whitelistRoot, 193 | }, 194 | }) 195 | .accounts({ 196 | signer: bucketCreatorKeypair.publicKey, 197 | dropletMint: dropletMintNew, 198 | }) 199 | .signers([dropletMintKeypairNew, bucketCreatorKeypair]) 200 | .rpc() 201 | ); 202 | }); 203 | 204 | it("can start migration", async () => { 205 | // Start migration 206 | await provider.connection.confirmTransaction( 207 | await program.methods 208 | .startMigration() 209 | .accounts({ 210 | signer: SOLVENT_ADMIN.publicKey, 211 | dropletMintOld, 212 | dropletMintNew, 213 | }) 214 | .signers([SOLVENT_ADMIN]) 215 | .rpc() 216 | ); 217 | }); 218 | 219 | it("fails to start migration when signer is not Solvent admin", async () => { 220 | // Unauthorized user trying to start migration 221 | const maliciousActorKeypair = await createKeypair(provider); 222 | 223 | try { 224 | await program.methods 225 | .startMigration() 226 | .accounts({ 227 | signer: maliciousActorKeypair.publicKey, 228 | dropletMintOld, 229 | dropletMintNew, 230 | }) 231 | .signers([maliciousActorKeypair]) 232 | .rpc(); 233 | } catch (error) { 234 | expect(error.message).contains("You do not have administrator access."); 235 | return; 236 | } 237 | expect.fail( 238 | "Program did not fail while starting migration with unauthorized signer." 239 | ); 240 | }); 241 | 242 | afterEach(async () => { 243 | dropletMintOld = undefined; 244 | dropletMintNew = undefined; 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /tests/tests/misc/claim-balance.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { 3 | program, 4 | provider, 5 | SOLVENT_ADMIN, 6 | SOLVENT_AUTHORITY_SEED, 7 | SOLVENT_CORE_TREASURY as SOLVENT_TREASURY, 8 | } from "../common"; 9 | import { expect } from "chai"; 10 | 11 | describe("Claiming balance of Solvent's PDAs", () => { 12 | let solventAuthority: anchor.web3.PublicKey; 13 | 14 | before(async () => { 15 | await provider.connection.confirmTransaction( 16 | await provider.connection.requestAirdrop( 17 | SOLVENT_ADMIN.publicKey, 18 | 10 * anchor.web3.LAMPORTS_PER_SOL 19 | ) 20 | ); 21 | 22 | [solventAuthority] = await anchor.web3.PublicKey.findProgramAddress( 23 | [SOLVENT_AUTHORITY_SEED], 24 | program.programId 25 | ); 26 | 27 | // Airdrop 10 SOLs to the Solvent authority PDA 28 | await provider.connection.confirmTransaction( 29 | await provider.connection.requestAirdrop( 30 | solventAuthority, 31 | 10 * anchor.web3.LAMPORTS_PER_SOL 32 | ) 33 | ); 34 | }); 35 | 36 | it("can claim balance of Solvent's PDAs", async () => { 37 | const minRentExcemptAmount = 38 | await provider.connection.getMinimumBalanceForRentExemption(0); 39 | 40 | // Get initial balances 41 | const initialSolventAuthorityBalance = await provider.connection.getBalance( 42 | solventAuthority 43 | ); 44 | const initialSolventTreasuryBalance = await provider.connection.getBalance( 45 | SOLVENT_TREASURY 46 | ); 47 | 48 | // Claim balance 49 | await provider.connection.confirmTransaction( 50 | await program.methods 51 | .claimBalance() 52 | .accounts({ 53 | signer: SOLVENT_ADMIN.publicKey, 54 | solventTreasury: SOLVENT_TREASURY, 55 | solventAuthority, 56 | }) 57 | .signers([SOLVENT_ADMIN]) 58 | .rpc() 59 | ); 60 | 61 | // Check the SOL balances got transferred 62 | expect(await provider.connection.getBalance(solventAuthority)).to.equal( 63 | minRentExcemptAmount 64 | ); 65 | expect( 66 | (await provider.connection.getBalance(SOLVENT_TREASURY)) - 67 | initialSolventTreasuryBalance 68 | ).to.equal(initialSolventAuthorityBalance - minRentExcemptAmount); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/tests/staking/stake-nft.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { createKeypair, mintNft, verifyCollection } from "../../utils"; 3 | import { 4 | program, 5 | provider, 6 | getGemFarm, 7 | SOLVENT_ADMIN, 8 | NftInfo, 9 | SOLVENT_AUTHORITY_SEED, 10 | FARMER_AUTHORITY_SEED, 11 | getGemBank, 12 | } from "../common"; 13 | import { beforeEach } from "mocha"; 14 | import { 15 | createMint, 16 | getAccount, 17 | getAssociatedTokenAddress, 18 | getOrCreateAssociatedTokenAccount, 19 | } from "@solana/spl-token-latest"; 20 | import { 21 | feeAccount, 22 | findFarmerPDA, 23 | GEM_BANK_PROG_ID, 24 | GEM_FARM_PROG_ID, 25 | RewardType, 26 | } from "@gemworks/gem-farm-ts"; 27 | import { assert, expect } from "chai"; 28 | 29 | describe("Staking NFT", () => { 30 | const nftSymbol = "DAPE"; 31 | const gemFarm = getGemFarm(GEM_FARM_PROG_ID, GEM_BANK_PROG_ID); 32 | const gemBank = getGemBank(GEM_BANK_PROG_ID); 33 | 34 | let solventAuthorityAddress: anchor.web3.PublicKey; 35 | 36 | before(async () => { 37 | await provider.connection.confirmTransaction( 38 | await provider.connection.requestAirdrop( 39 | SOLVENT_ADMIN.publicKey, 40 | 10 * anchor.web3.LAMPORTS_PER_SOL 41 | ) 42 | ); 43 | 44 | [solventAuthorityAddress] = await anchor.web3.PublicKey.findProgramAddress( 45 | [SOLVENT_AUTHORITY_SEED], 46 | program.programId 47 | ); 48 | }); 49 | 50 | let dropletMint: anchor.web3.PublicKey; 51 | let farm: anchor.web3.PublicKey, bank: anchor.web3.PublicKey; 52 | 53 | const nftInfos: NftInfo[] = []; 54 | 55 | beforeEach(async () => { 56 | // An NFT enthusiast wants to create a bucket for an NFT collection 57 | const userKeypair = await createKeypair(provider); 58 | 59 | // Create the bucket address 60 | const dropletMintKeypair = new anchor.web3.Keypair(); 61 | dropletMint = dropletMintKeypair.publicKey; 62 | 63 | // Create the collection NFT 64 | const collectionCreatorKeypair = await createKeypair(provider); 65 | const { mint: collectionMint } = await mintNft( 66 | provider, 67 | nftSymbol, 68 | collectionCreatorKeypair, 69 | collectionCreatorKeypair.publicKey 70 | ); 71 | 72 | // Minting 3 NFTs and sending them to 3 different users 73 | for (const i of Array(3)) { 74 | // Generate NFT creator and holder keypairs 75 | const holderKeypair = await createKeypair(provider); 76 | const creatorKeypair = await createKeypair(provider); 77 | 78 | // Creator mints an NFT and sends it to holder 79 | const { mint, metadata } = await mintNft( 80 | provider, 81 | nftSymbol, 82 | creatorKeypair, 83 | holderKeypair.publicKey, 84 | collectionMint 85 | ); 86 | 87 | // Collection authority verifies that the NFT belongs to the collection 88 | await verifyCollection( 89 | provider, 90 | mint, 91 | collectionMint, 92 | collectionCreatorKeypair 93 | ); 94 | 95 | // Set public vars' values 96 | nftInfos.push({ 97 | nftMintAddress: mint, 98 | nftMetadataAddress: metadata, 99 | holderKeypair, 100 | }); 101 | } 102 | 103 | // Create bucket on Solvent 104 | await provider.connection.confirmTransaction( 105 | await program.methods 106 | // @ts-ignore 107 | .createBucket({ v2: { collectionMint } }) 108 | .accounts({ 109 | signer: userKeypair.publicKey, 110 | dropletMint: dropletMint, 111 | }) 112 | .signers([dropletMintKeypair, userKeypair]) 113 | .rpc() 114 | ); 115 | 116 | // Create farm 117 | const bankKeypair = new anchor.web3.Keypair(); 118 | const farmKeypair = new anchor.web3.Keypair(); 119 | const farmManagerKeypair = await createKeypair(provider); 120 | const rewardAMint = await createMint( 121 | provider.connection, 122 | farmManagerKeypair, 123 | farmManagerKeypair.publicKey, 124 | null, 125 | 10 ^ 9 126 | ); 127 | const rewardBMint = await createMint( 128 | provider.connection, 129 | farmManagerKeypair, 130 | farmManagerKeypair.publicKey, 131 | null, 132 | 10 ^ 9 133 | ); 134 | 135 | await gemFarm.initFarm( 136 | farmKeypair, 137 | farmManagerKeypair, 138 | farmManagerKeypair, 139 | bankKeypair, 140 | rewardAMint, 141 | RewardType.Fixed, 142 | rewardBMint, 143 | RewardType.Fixed, 144 | { 145 | minStakingPeriodSec: new anchor.BN(0), 146 | cooldownPeriodSec: new anchor.BN(0), 147 | unstakingFeeLamp: new anchor.BN(1000000), 148 | } 149 | ); 150 | 151 | bank = bankKeypair.publicKey; 152 | farm = farmKeypair.publicKey; 153 | 154 | // Update staking params 155 | await provider.connection.confirmTransaction( 156 | await program.methods 157 | .updateStakingParams() 158 | .accounts({ 159 | signer: SOLVENT_ADMIN.publicKey, 160 | dropletMint, 161 | gemworksFarm: farm, 162 | gemfarmProgram: GEM_FARM_PROG_ID, 163 | gembankProgram: GEM_BANK_PROG_ID, 164 | gemworksFeeAccount: feeAccount, 165 | }) 166 | .signers([SOLVENT_ADMIN]) 167 | .rpc() 168 | ); 169 | 170 | // Enable staking 171 | await provider.connection.confirmTransaction( 172 | await program.methods 173 | .setStakingEnabled(true) 174 | .accounts({ 175 | signer: SOLVENT_ADMIN.publicKey, 176 | dropletMint, 177 | }) 178 | .signers([SOLVENT_ADMIN]) 179 | .rpc() 180 | ); 181 | 182 | // Looping through all the NFTs and depositing them in the bucket 183 | for (const { 184 | nftMintAddress, 185 | nftMetadataAddress, 186 | holderKeypair, 187 | } of nftInfos) { 188 | // NFT holder's NFT account 189 | let holderNftTokenAccount = await getOrCreateAssociatedTokenAccount( 190 | provider.connection, 191 | holderKeypair, 192 | nftMintAddress, 193 | holderKeypair.publicKey 194 | ); 195 | 196 | // Bucket's NFT account, a PDA owned by the Solvent program 197 | const solventNftTokenAccount = await getAssociatedTokenAddress( 198 | nftMintAddress, 199 | solventAuthorityAddress, 200 | true 201 | ); 202 | 203 | // The holder's droplet account 204 | let holderDropletTokenAccount = await getOrCreateAssociatedTokenAccount( 205 | provider.connection, 206 | holderKeypair, 207 | dropletMint, 208 | holderKeypair.publicKey 209 | ); 210 | 211 | // Deposit NFT into Solvent 212 | await provider.connection.confirmTransaction( 213 | await program.methods 214 | .depositNft(false, null) 215 | .accounts({ 216 | signer: holderKeypair.publicKey, 217 | dropletMint, 218 | nftMint: nftMintAddress, 219 | nftMetadata: nftMetadataAddress, 220 | signerNftTokenAccount: holderNftTokenAccount.address, 221 | solventNftTokenAccount, 222 | destinationDropletTokenAccount: holderDropletTokenAccount.address, 223 | }) 224 | .signers([holderKeypair]) 225 | .rpc() 226 | ); 227 | } 228 | }); 229 | 230 | it("can stake NFT", async () => { 231 | // Looping through all the NFTs and staking them 232 | for (const { nftMintAddress } of nftInfos) { 233 | const randomKeypair = await createKeypair(provider); 234 | const solventNftTokenAccount = await getAssociatedTokenAddress( 235 | nftMintAddress, 236 | solventAuthorityAddress, 237 | true 238 | ); 239 | 240 | const [farmerAuthorityAddress] = 241 | await anchor.web3.PublicKey.findProgramAddress( 242 | [FARMER_AUTHORITY_SEED, nftMintAddress.toBuffer()], 243 | program.programId 244 | ); 245 | 246 | const farmerNftTokenAccount = await getAssociatedTokenAddress( 247 | nftMintAddress, 248 | farmerAuthorityAddress, 249 | true 250 | ); 251 | 252 | // Stake NFT 253 | await provider.connection.confirmTransaction( 254 | await program.methods 255 | .stakeNft() 256 | .accounts({ 257 | signer: randomKeypair.publicKey, 258 | dropletMint, 259 | gembankProgram: GEM_BANK_PROG_ID, 260 | gemfarmProgram: GEM_FARM_PROG_ID, 261 | gemworksBank: bank, 262 | gemworksFarm: farm, 263 | gemworksFeeAccount: feeAccount, 264 | nftMint: nftMintAddress, 265 | solventNftTokenAccount, 266 | farmerNftTokenAccount, 267 | }) 268 | .signers([randomKeypair]) 269 | .rpc() 270 | ); 271 | 272 | // Ensure solvent does not have the NFT anymore 273 | expect( 274 | (await getAccount(provider.connection, solventNftTokenAccount)).amount 275 | ).to.equal(0n); 276 | 277 | // Assert farmer account has correct info 278 | const [farmerAddress] = await findFarmerPDA(farm, farmerAuthorityAddress); 279 | const farmer = await gemFarm.fetchFarmerAcc(farmerAddress); 280 | expect(farmer.farm.toBase58()).to.equal(farm.toBase58()); 281 | expect(farmer.identity.toBase58()).to.equal( 282 | farmerAuthorityAddress.toBase58() 283 | ); 284 | assert("staked" in farmer.state); 285 | expect(farmer.gemsStaked.toNumber()).to.equal(1); 286 | 287 | // Assert vault account has correct info 288 | const vault = await gemBank.fetchVaultAcc(farmer.vault); 289 | expect(vault.locked).to.be.true; 290 | expect(vault.gemCount.toNumber()).to.equal(1); 291 | } 292 | }); 293 | 294 | afterEach(() => { 295 | dropletMint = undefined; 296 | farm = undefined; 297 | bank = undefined; 298 | nftInfos.length = 0; 299 | }); 300 | }); 301 | -------------------------------------------------------------------------------- /tests/tests/staking/update-staking-params.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { createKeypair, mintNft } from "../../utils"; 3 | import { 4 | program, 5 | provider, 6 | getGemFarm, 7 | SOLVENT_ADMIN, 8 | BUCKET_SEED, 9 | } from "../common"; 10 | import { beforeEach } from "mocha"; 11 | import { createMint } from "@solana/spl-token-latest"; 12 | import { 13 | feeAccount, 14 | GEM_BANK_PROG_ID, 15 | GEM_FARM_PROG_ID, 16 | RewardType, 17 | } from "@gemworks/gem-farm-ts"; 18 | import { assert, expect } from "chai"; 19 | 20 | describe("Updating staking related params", () => { 21 | const nftSymbol = "DAPE"; 22 | 23 | const gemFarm = getGemFarm(GEM_FARM_PROG_ID, GEM_BANK_PROG_ID); 24 | 25 | let dropletMint: anchor.web3.PublicKey, 26 | bucketStateAddress: anchor.web3.PublicKey; 27 | 28 | let farm: anchor.web3.PublicKey; 29 | 30 | before(async () => { 31 | await provider.connection.confirmTransaction( 32 | await provider.connection.requestAirdrop( 33 | SOLVENT_ADMIN.publicKey, 34 | 10 * anchor.web3.LAMPORTS_PER_SOL 35 | ) 36 | ); 37 | }); 38 | 39 | beforeEach(async () => { 40 | // An NFT enthusiast wants to create a bucket for an NFT collection 41 | const userKeypair = await createKeypair(provider); 42 | 43 | // Create the bucket address 44 | const dropletMintKeypair = new anchor.web3.Keypair(); 45 | dropletMint = dropletMintKeypair.publicKey; 46 | [bucketStateAddress] = await anchor.web3.PublicKey.findProgramAddress( 47 | [dropletMintKeypair.publicKey.toBuffer(), BUCKET_SEED], 48 | program.programId 49 | ); 50 | 51 | // Create the collection NFT 52 | const collectionCreatorKeypair = await createKeypair(provider); 53 | const { mint: collectionMint } = await mintNft( 54 | provider, 55 | nftSymbol, 56 | collectionCreatorKeypair, 57 | collectionCreatorKeypair.publicKey 58 | ); 59 | 60 | // Create bucket on Solvent 61 | await provider.connection.confirmTransaction( 62 | await program.methods 63 | // @ts-ignore 64 | .createBucket({ v2: { collectionMint } }) 65 | .accounts({ 66 | signer: userKeypair.publicKey, 67 | dropletMint: dropletMint, 68 | }) 69 | .signers([dropletMintKeypair, userKeypair]) 70 | .rpc() 71 | ); 72 | 73 | // Create farm 74 | const bankKeypair = new anchor.web3.Keypair(); 75 | const farmKeypair = new anchor.web3.Keypair(); 76 | const farmManagerKeypair = await createKeypair(provider); 77 | const rewardAMint = await createMint( 78 | provider.connection, 79 | farmManagerKeypair, 80 | farmManagerKeypair.publicKey, 81 | null, 82 | 10 ^ 9 83 | ); 84 | const rewardBMint = await createMint( 85 | provider.connection, 86 | farmManagerKeypair, 87 | farmManagerKeypair.publicKey, 88 | null, 89 | 10 ^ 9 90 | ); 91 | 92 | await gemFarm.initFarm( 93 | farmKeypair, 94 | farmManagerKeypair, 95 | farmManagerKeypair, 96 | bankKeypair, 97 | rewardAMint, 98 | RewardType.Fixed, 99 | rewardBMint, 100 | RewardType.Fixed, 101 | { 102 | minStakingPeriodSec: new anchor.BN(0), 103 | cooldownPeriodSec: new anchor.BN(0), 104 | unstakingFeeLamp: new anchor.BN(1000000), 105 | } 106 | ); 107 | 108 | farm = farmKeypair.publicKey; 109 | }); 110 | 111 | it("can update staking params", async () => { 112 | await provider.connection.confirmTransaction( 113 | await program.methods 114 | .updateStakingParams() 115 | .accounts({ 116 | signer: SOLVENT_ADMIN.publicKey, 117 | dropletMint, 118 | gemworksFarm: farm, 119 | gemfarmProgram: GEM_FARM_PROG_ID, 120 | gembankProgram: GEM_BANK_PROG_ID, 121 | gemworksFeeAccount: feeAccount, 122 | }) 123 | .signers([SOLVENT_ADMIN]) 124 | .rpc() 125 | ); 126 | 127 | // Ensure bucket has correct contents 128 | const bucketState = await program.account.bucketStateV3.fetch( 129 | bucketStateAddress 130 | ); 131 | expect(bucketState.stakingParams.gembankProgram.toBase58()).to.equal( 132 | GEM_BANK_PROG_ID.toBase58() 133 | ); 134 | expect(bucketState.stakingParams.gemfarmProgram.toBase58()).to.equal( 135 | GEM_FARM_PROG_ID.toBase58() 136 | ); 137 | expect(bucketState.stakingParams.gemworksFarm.toBase58()).to.equal( 138 | farm.toBase58() 139 | ); 140 | expect(bucketState.stakingParams.gemworksFeeAccount.toBase58()).to.equal( 141 | feeAccount.toBase58() 142 | ); 143 | }); 144 | 145 | it("fails to update staking params with invalid farm", async () => { 146 | // Create farm 147 | const bankKeypair = new anchor.web3.Keypair(); 148 | const farmKeypair = new anchor.web3.Keypair(); 149 | const farmManagerKeypair = await createKeypair(provider); 150 | const rewardAMint = await createMint( 151 | provider.connection, 152 | farmManagerKeypair, 153 | farmManagerKeypair.publicKey, 154 | null, 155 | 10 ^ 9 156 | ); 157 | const rewardBMint = await createMint( 158 | provider.connection, 159 | farmManagerKeypair, 160 | farmManagerKeypair.publicKey, 161 | null, 162 | 10 ^ 9 163 | ); 164 | 165 | await gemFarm.initFarm( 166 | farmKeypair, 167 | farmManagerKeypair, 168 | farmManagerKeypair, 169 | bankKeypair, 170 | rewardAMint, 171 | RewardType.Fixed, 172 | rewardBMint, 173 | RewardType.Fixed, 174 | { 175 | minStakingPeriodSec: new anchor.BN(100), 176 | cooldownPeriodSec: new anchor.BN(100), 177 | unstakingFeeLamp: new anchor.BN(890880), 178 | } 179 | ); 180 | 181 | const invalidFarm = farmKeypair.publicKey; 182 | 183 | try { 184 | await provider.connection.confirmTransaction( 185 | await program.methods 186 | .updateStakingParams() 187 | .accounts({ 188 | signer: SOLVENT_ADMIN.publicKey, 189 | dropletMint, 190 | gemworksFarm: invalidFarm, 191 | gemfarmProgram: GEM_FARM_PROG_ID, 192 | gembankProgram: GEM_BANK_PROG_ID, 193 | gemworksFeeAccount: feeAccount, 194 | }) 195 | .signers([SOLVENT_ADMIN]) 196 | .rpc() 197 | ); 198 | } catch (error) { 199 | assert.include(error.message, "The farm is not suitable for staking."); 200 | return; 201 | } 202 | expect.fail( 203 | "Program did not fail while updating staking params with invalid farm." 204 | ); 205 | }); 206 | 207 | it("fails to update staking params when signer is not Solvent admin", async () => { 208 | const randomKeypair = await createKeypair(provider); 209 | try { 210 | await program.methods 211 | .updateStakingParams() 212 | .accounts({ 213 | signer: randomKeypair.publicKey, 214 | dropletMint, 215 | gemworksFarm: farm, 216 | gemfarmProgram: GEM_FARM_PROG_ID, 217 | gembankProgram: GEM_BANK_PROG_ID, 218 | gemworksFeeAccount: feeAccount, 219 | }) 220 | .signers([randomKeypair]) 221 | .rpc(); 222 | } catch (error) { 223 | expect(error.message).to.contain("You do not have administrator access"); 224 | return; 225 | } 226 | expect.fail( 227 | "Program did not fail while updating staking params when signer is not Solvent admin" 228 | ); 229 | }); 230 | 231 | afterEach(() => { 232 | dropletMint = undefined; 233 | bucketStateAddress = undefined; 234 | farm = undefined; 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { 3 | createAssociatedTokenAccount, 4 | createMint, 5 | getAccount, 6 | mintToChecked, 7 | } from "@solana/spl-token-latest"; 8 | import { 9 | PROGRAM_ID as METADATA_PROGRAM_ID, 10 | createCreateMetadataAccountV2Instruction, 11 | createCreateMetadataAccountInstruction, 12 | createCreateMasterEditionV3Instruction, 13 | createVerifyCollectionInstruction, 14 | } from "@metaplex-foundation/mpl-token-metadata"; 15 | import keccak256 from "keccak256"; 16 | import MerkleTree from "merkletreejs"; 17 | 18 | export const createKeypair = async (provider: anchor.Provider) => { 19 | const keypair = new anchor.web3.Keypair(); 20 | const txn = await provider.connection.requestAirdrop( 21 | keypair.publicKey, 22 | 10 * anchor.web3.LAMPORTS_PER_SOL 23 | ); 24 | await provider.connection.confirmTransaction(txn); 25 | return keypair; 26 | }; 27 | 28 | const getTokenMetadata = async (tokenMint: anchor.web3.PublicKey) => { 29 | const [tokenMetadataAddress, bump] = 30 | await anchor.web3.PublicKey.findProgramAddress( 31 | [ 32 | Buffer.from("metadata"), 33 | METADATA_PROGRAM_ID.toBuffer(), 34 | tokenMint.toBuffer(), 35 | ], 36 | METADATA_PROGRAM_ID 37 | ); 38 | return tokenMetadataAddress; 39 | }; 40 | 41 | const getTokenEdition = async (tokenMint: anchor.web3.PublicKey) => { 42 | const [tokenMetadataAddress, bump] = 43 | await anchor.web3.PublicKey.findProgramAddress( 44 | [ 45 | Buffer.from("metadata"), 46 | METADATA_PROGRAM_ID.toBuffer(), 47 | tokenMint.toBuffer(), 48 | Buffer.from("edition"), 49 | ], 50 | METADATA_PROGRAM_ID 51 | ); 52 | return tokenMetadataAddress; 53 | }; 54 | 55 | export const mintNft = async ( 56 | provider: anchor.Provider, 57 | symbol: string, 58 | creator: anchor.web3.Keypair, 59 | destination: anchor.web3.PublicKey, 60 | collectionMint?: anchor.web3.PublicKey, 61 | v1: boolean = false, 62 | sellerFeeBasisPoints: number = 10 63 | ) => { 64 | const mint = await createMint( 65 | provider.connection, 66 | creator, 67 | creator.publicKey, 68 | null, 69 | 0 70 | ); 71 | 72 | const tokenAccount = await createAssociatedTokenAccount( 73 | provider.connection, 74 | creator, 75 | mint, 76 | destination 77 | ); 78 | 79 | await mintToChecked( 80 | provider.connection, 81 | creator, 82 | mint, 83 | tokenAccount, 84 | creator.publicKey, 85 | 1, 86 | 0 87 | ); 88 | 89 | const transaction = new anchor.web3.Transaction(); 90 | 91 | // Set Metadata 92 | const metadata = await getTokenMetadata(mint); 93 | v1 94 | ? transaction.add( 95 | createCreateMetadataAccountInstruction( 96 | { 97 | metadata, 98 | mint, 99 | mintAuthority: creator.publicKey, 100 | updateAuthority: creator.publicKey, 101 | payer: creator.publicKey, 102 | }, 103 | { 104 | createMetadataAccountArgs: { 105 | isMutable: false, 106 | data: { 107 | name: "Pretty Cool NFT", 108 | symbol, 109 | sellerFeeBasisPoints, 110 | uri: "https://pretty-cool-nft.xyz/metadata", 111 | creators: [ 112 | { 113 | address: creator.publicKey, 114 | share: 100, 115 | verified: true, 116 | }, 117 | ], 118 | }, 119 | }, 120 | } 121 | ) 122 | ) 123 | : transaction.add( 124 | createCreateMetadataAccountV2Instruction( 125 | { 126 | metadata, 127 | mint, 128 | mintAuthority: creator.publicKey, 129 | updateAuthority: creator.publicKey, 130 | payer: creator.publicKey, 131 | }, 132 | { 133 | createMetadataAccountArgsV2: { 134 | isMutable: false, 135 | data: { 136 | name: "Pretty Cool NFT", 137 | symbol, 138 | sellerFeeBasisPoints, 139 | uri: "https://pretty-cool-nft.xyz/metadata", 140 | creators: [ 141 | { 142 | address: creator.publicKey, 143 | share: 100, 144 | verified: true, 145 | }, 146 | ], 147 | collection: collectionMint 148 | ? { key: collectionMint, verified: false } 149 | : null, 150 | uses: null, 151 | }, 152 | }, 153 | } 154 | ) 155 | ); 156 | 157 | // Create master edition 158 | const edition = await getTokenEdition(mint); 159 | transaction.add( 160 | createCreateMasterEditionV3Instruction( 161 | { 162 | edition, 163 | mint, 164 | updateAuthority: creator.publicKey, 165 | mintAuthority: creator.publicKey, 166 | payer: creator.publicKey, 167 | metadata, 168 | }, 169 | { createMasterEditionArgs: { maxSupply: 0 } } 170 | ) 171 | ); 172 | 173 | await provider.sendAndConfirm(transaction, [creator]); 174 | 175 | return { mint, metadata, edition }; 176 | }; 177 | 178 | export const verifyCollection = async ( 179 | provider: anchor.AnchorProvider, 180 | nftMint: anchor.web3.PublicKey, 181 | collectionMint: anchor.web3.PublicKey, 182 | collectionAuthority: anchor.web3.Keypair 183 | ) => { 184 | // Setup: Verify collection of the NFT 185 | const transaction = new anchor.web3.Transaction(); 186 | transaction.add( 187 | createVerifyCollectionInstruction({ 188 | metadata: await getTokenMetadata(nftMint), 189 | collectionAuthority: collectionAuthority.publicKey, 190 | payer: provider.wallet.publicKey, 191 | collectionMint: collectionMint, 192 | collection: await getTokenMetadata(collectionMint), 193 | collectionMasterEditionAccount: await getTokenEdition(collectionMint), 194 | }) 195 | ); 196 | return provider.sendAndConfirm(transaction, [collectionAuthority]); 197 | }; 198 | 199 | export const getMerkleTree = (mints: anchor.web3.PublicKey[]) => { 200 | const leaves = mints.map((x) => keccak256(x.toBuffer())); 201 | const tree = new MerkleTree(leaves, keccak256, { sort: true }); 202 | const root = tree.getRoot(); 203 | return { root: [...root], tree }; 204 | }; 205 | 206 | export const getMerkleProof = ( 207 | tree: MerkleTree, 208 | mint: anchor.web3.PublicKey 209 | ) => { 210 | const leaf = keccak256(mint.toBuffer()); 211 | const proof: Buffer[] = tree.getProof(leaf).map((x) => x.data); 212 | return proof.map((x) => [...x]); 213 | }; 214 | 215 | export const getBalance = async ( 216 | connection: anchor.web3.Connection, 217 | tokenAccount: anchor.web3.PublicKey 218 | ) => { 219 | let tokenAccountBalance = BigInt(0); 220 | try { 221 | tokenAccountBalance = (await getAccount(connection, tokenAccount)).amount; 222 | } catch (error) {} 223 | return tokenAccountBalance; 224 | }; 225 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai", "node"], 4 | "typeRoots": ["./node_modules/@types/**"], 5 | "module": "commonjs", 6 | "target": "es2020", 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------