├── .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 |
--------------------------------------------------------------------------------