Compare commits

..

122 Commits

Author SHA1 Message Date
Jon Cinque 731bb6f57a
stake-pool: Bump version for release (#1707) 2021-05-11 21:46:49 +00:00
Jon Cinque cebb3176b2
stake-pool: Add helper instruction creators for stake pool integration (#1706)
* stake-pool: Add more instructions for easier usage

* Add extra check (shouldn't be necessary, but who knows?)
2021-05-11 20:48:58 +00:00
drbh f3a8fae2f5
swap associated token account addr for token addr (#1705)
the associated token account address is derived from token address. The token address is the correct argument.
2021-05-11 13:45:15 -06:00
dependabot[bot] 3709ac60db
build(deps-dev): bump prettier from 2.2.1 to 2.3.0 in /token-swap/js (#1703)
Bumps [prettier](https://github.com/prettier/prettier) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.2.1...2.3.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 14:05:42 +00:00
dependabot[bot] 5a9eebd692
build(deps): bump @solana/web3.js in /token-swap/js (#1702)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.10.0...v1.10.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 13:30:21 +00:00
dependabot[bot] d3d527e26b
build(deps): bump dotenv from 8.2.0 to 9.0.2 in /token-swap/js (#1701)
Bumps [dotenv](https://github.com/motdotla/dotenv) from 8.2.0 to 9.0.2.
- [Release notes](https://github.com/motdotla/dotenv/releases)
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v8.2.0...v9.0.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 12:57:47 +00:00
dependabot[bot] 88eb28625b
build(deps-dev): bump start-server-and-test in /token-swap/js (#1700)
Bumps [start-server-and-test](https://github.com/bahmutov/start-server-and-test) from 1.12.0 to 1.12.1.
- [Release notes](https://github.com/bahmutov/start-server-and-test/releases)
- [Commits](https://github.com/bahmutov/start-server-and-test/compare/v1.12.0...v1.12.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 12:25:58 +00:00
dependabot[bot] e7360196ae
build(deps): bump @solana/web3.js in /token-lending/js (#1699)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.10.0...v1.10.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 11:48:30 +00:00
dependabot[bot] 0007114121
build(deps-dev): bump eslint from 7.21.0 to 7.26.0 in /token-swap/js (#1698)
Bumps [eslint](https://github.com/eslint/eslint) from 7.21.0 to 7.26.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.21.0...v7.26.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 11:47:39 +00:00
dependabot[bot] 848c580889
build(deps-dev): bump @typescript-eslint/parser in /token-lending/js (#1695)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.22.1 to 4.23.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.23.0/packages/parser)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 11:08:29 +00:00
dependabot[bot] 7f3ccec305
build(deps): bump buffer-layout from 1.2.0 to 1.2.1 in /token-swap/js (#1696)
Bumps [buffer-layout](https://github.com/pabigot/buffer-layout) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/pabigot/buffer-layout/releases)
- [Changelog](https://github.com/pabigot/buffer-layout/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pabigot/buffer-layout/commits/v1.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 10:34:31 +00:00
dr497 1f48b5bc5e docs: add token vesting section 2021-05-10 20:11:23 -07:00
dependabot[bot] 8a02ab5650
build(deps-dev): bump @typescript-eslint/eslint-plugin (#1694)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.22.1 to 4.23.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.23.0/packages/eslint-plugin)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 22:58:19 +00:00
dependabot[bot] 7336a7641e
build(deps-dev): bump dotenv from 9.0.1 to 9.0.2 in /token-lending/js (#1693)
Bumps [dotenv](https://github.com/motdotla/dotenv) from 9.0.1 to 9.0.2.
- [Release notes](https://github.com/motdotla/dotenv/releases)
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v9.0.1...v9.0.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 22:21:06 +00:00
dependabot[bot] e402aed8dd
build(deps): bump @solana/web3.js in /token-lending/js (#1692)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.9.1 to 1.10.0.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.9.1...v1.10.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 21:45:45 +00:00
dependabot[bot] 96100f751b
build(deps-dev): bump eslint from 7.25.0 to 7.26.0 in /token-lending/js (#1691)
Bumps [eslint](https://github.com/eslint/eslint) from 7.25.0 to 7.26.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.25.0...v7.26.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 21:07:59 +00:00
Pierre 6ac82f9caf
token-swap typescript: Move things around, fix typing problems (#1643)
* Move things around, fix typing problems

* oups output built in src

* fix typegen

* fix cli

* fix linting, apply review comments and add forgotten package-lock.json change

* remove typegen.sh and move simpler command to package.json

* Move things around, remove babel and rollup

* Update packages

* Add browser dist to eslintignore

Co-authored-by: Arrowana <8245419+Arrowana@users.noreply.github.com>
Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
2021-05-10 22:30:13 +02:00
dependabot[bot] 25b51bc2a0
build(deps-dev): bump dotenv from 9.0.0 to 9.0.1 in /token-lending/js (#1690)
Bumps [dotenv](https://github.com/motdotla/dotenv) from 9.0.0 to 9.0.1.
- [Release notes](https://github.com/motdotla/dotenv/releases)
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 13:49:55 +00:00
dependabot[bot] f0ad7b3d5f
build(deps): bump @solana/spl-token in /token-lending/js (#1688)
Bumps [@solana/spl-token](https://github.com/solana-labs/solana-program-library) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/solana-labs/solana-program-library/releases)
- [Commits](https://github.com/solana-labs/solana-program-library/compare/@solana/spl-token@v0.1.3...@solana/spl-token@v0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 13:03:57 +00:00
dependabot[bot] 2522d937d2
build(deps): bump hosted-git-info from 2.8.8 to 2.8.9 in /token-swap/js (#1689)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 13:01:45 +00:00
Jon Cinque d68608c440
stake-pool: Re-enable tests, avoid warping so much (#1687) 2021-05-10 12:33:56 +00:00
dependabot[bot] c6a9446b17
build(deps-dev): bump @rollup/plugin-commonjs in /token-lending/js (#1684)
Bumps [@rollup/plugin-commonjs](https://github.com/rollup/plugins/tree/HEAD/packages/commonjs) from 18.0.0 to 19.0.0.
- [Release notes](https://github.com/rollup/plugins/releases)
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/commonjs/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/commonjs-v19.0.0/packages/commonjs)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 12:27:05 +00:00
drbh 0ac20fcddf incorrect capitalization 2021-05-08 18:50:24 -07:00
drbh 9ca1c6e8f2 adjust typos and proper nouns on naming service page 2021-05-08 18:50:24 -07:00
Tyera Eulberg 5a5f129694
Ensure only docs files are modified (#1682) 2021-05-08 17:25:46 -06:00
Tyera Eulberg 13689ac2dd
Account with 2 Cs (#1680) 2021-05-08 19:18:32 +00:00
Christian Machacek 18d96ffce6 Fix cross-program-invocation C example
Correctly check if deriving the PDA with the given seed was successful.
Don't pass the system program pubkey to the allocate instruction: the first account passed must be the one to be allocated.
2021-05-08 09:58:39 -07:00
jordansexton f0b1cec4d8 stake-pool: minor typo fix 2021-05-07 13:50:35 -05:00
dependabot[bot] cfc6c582ff
build(deps): bump lodash from 4.17.19 to 4.17.21 in /token-swap/js (#1674)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-07 09:55:28 +00:00
dependabot[bot] 090ecefa46
build(deps): bump lodash from 4.17.20 to 4.17.21 in /token-lending/js (#1672)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-07 09:14:28 +00:00
Trent Nelson 9482f8d4e3 token/js: Bump version to v0.1.4 2021-05-07 06:39:04 +00:00
jordansexton 1526301d0d ran cargo fmt 2021-05-06 23:32:14 -05:00
Leonard G 3045d7b1df
Flashloan for token lending (#1444)
Co-authored-by: Justin Starry <justin.m.starry@gmail.com>
Co-authored-by: Jordan Sexton <jordan@jordansexton.com>
2021-05-06 23:27:41 -05:00
dependabot[bot] ea4b7e62fc
build(deps-dev): bump @typescript-eslint/eslint-plugin in /token/js (#1669)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.17.0 to 4.22.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.22.1/packages/eslint-plugin)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 10:51:41 +00:00
dependabot[bot] 51cc3ed6fe
build(deps): bump @babel/runtime from 7.13.10 to 7.14.0 in /token/js (#1668)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.13.10 to 7.14.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.0/packages/babel-runtime)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 10:15:04 +00:00
dependabot[bot] 9aa238e0b4
build(deps-dev): bump @rollup/plugin-node-resolve in /token/js (#1667)
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 11.2.0 to 13.0.0.
- [Release notes](https://github.com/rollup/plugins/releases)
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/commonjs-v13.0.0/packages/node-resolve)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 09:40:44 +00:00
Michael Vines 00eb817073
Update token.md 2021-05-05 21:06:48 -07:00
dependabot[bot] 4e1892adba
build(deps-dev): bump dotenv from 8.2.0 to 9.0.0 in /token-lending/js (#1666)
Bumps [dotenv](https://github.com/motdotla/dotenv) from 8.2.0 to 9.0.0.
- [Release notes](https://github.com/motdotla/dotenv/releases)
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v8.2.0...v9.0.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 00:07:46 +00:00
dependabot[bot] 3714c70d12
build(deps): bump @solana/web3.js in /token-lending/js (#1665)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.9.0...v1.9.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-05 23:33:23 +00:00
dependabot[bot] a05fd7185d
build(deps-dev): bump @types/node in /token-lending/js (#1664)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 15.0.1 to 15.0.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-05 22:58:30 +00:00
dependabot[bot] cf2fcde121
build(deps-dev): bump @typescript-eslint/eslint-plugin (#1663)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.22.0 to 4.22.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.22.1/packages/eslint-plugin)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-05 22:22:49 +00:00
Trent Nelson 3d04aa0109 chore: add negative test for ATA creation with off-curve owner 2021-05-05 15:48:59 -06:00
Trent Nelson 7320cf404b fix: reject off-curve owners during ATA creation 2021-05-05 15:48:59 -06:00
Trent Nelson 975b524487 chore: bump solana web3 to v1.9.1 2021-05-05 15:48:59 -06:00
Michael Vines a539ef021f Explicitly call out the --decimals argument to `spl-token create-token` 2021-05-05 08:47:55 -07:00
Jon Cinque 1efc90c5c9
stake-pool: Update docs for all new features (#1651)
The stake pool program changed a lot, but the docs did not.  Bring them
up to speed!
2021-05-05 16:14:24 +02:00
dependabot[bot] 0ce5b41b9f
build(deps-dev): bump @typescript-eslint/parser in /token-lending/js (#1661)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.22.0 to 4.22.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.22.1/packages/parser)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-05 09:41:51 +00:00
Trent Nelson 1f36ca301b Bump solana crates to v1.6.7 2021-05-05 04:39:49 +00:00
Michael Vines 4068b77f53 Add name service to sidebar 2021-05-04 10:20:01 -07:00
Solana Maintainers 512497aa39 use cleanup 2021-05-04 10:12:05 -07:00
Solana Maintainers d9fd11a8f3 Add program id 2021-05-04 10:12:05 -07:00
dependabot[bot] 9e4764faf7
build(deps-dev): bump rollup from 2.46.0 to 2.47.0 in /token-lending/js (#1658)
Bumps [rollup](https://github.com/rollup/rollup) from 2.46.0 to 2.47.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.46.0...v2.47.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-04 09:36:09 +00:00
Lcchy 0ed080a50f
Add name-service program and js bindings (#1600) 2021-05-04 00:07:11 +00:00
Jon Cinque 3dd6767297
stake-pool: Update versions for crate deployment (#1657) 2021-05-03 20:50:49 +00:00
dependabot[bot] b99c9b375c
build(deps): bump @solana/web3.js in /token-lending/js (#1655)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.7.1 to 1.9.0.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.7.1...v1.9.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 11:24:25 +00:00
Jon Cinque 9c63bc0b06
stake-pool-cli: All sorts of fixes (#1650)
While going back through the docs, I ended up doing a lot of the stake
pool CLI items:

* Deposit / withdraw command: Use associated token account by default
* Create command: Allow passing the stake pool and mint keypair (useful
  for testing)
* Create command: Split the transaction for pool creation (required to get under the
  transaction size limit)
* Add / remove validator command: take a validator vote account rather than stake
  account, which makes integration from outside tools a lot simpler
* Update command: add a `--force` flag to force the update
* Update command: add a `--no-merge` flag to not merge while updating
  (useful to allow the pool to work, even if the transient stake
  accounts are unmergeable)
* Withdraw: Add `--use-reserve` flag to withdraw from reserve
* Withdraw: Add `--vote-account` arg to specify which validator to
  withdraw from
2021-05-03 11:47:24 +02:00
dependabot[bot] 5a357a50df
build(deps): bump @solana/web3.js in /token-lending/js (#1648)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.7.0...v1.7.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-30 09:48:42 +00:00
Jon Cinque 0f4f2b8de9
token-swap: Fix slippage on withdraw tokens (#1637)
* token-swap: Fix withdrawal all tokens slippage

When withdrawing, the slippage check is done before `min`ing the token a
and b amounts, which makes it possible to ignore the desired slippage,
and lose out on a lot more than expected.

This has an additional knock-on effect. When burning all of the pool
tokens, it becomes impossible to ever use it again.

* Check for slippage after getting the actual amount that would be
  traded
* Re-initialize the pool token amount on the next deposit if all pool
  tokens were burned

Fixes #1629

* Fmt + clippy

* Deposit one side on 0 pool tokens just gives new supply back
2021-04-29 14:26:47 +02:00
dependabot[bot] 60ef11e26b
build(deps-dev): bump rollup from 2.45.2 to 2.46.0 in /token-lending/js (#1644)
Bumps [rollup](https://github.com/rollup/rollup) from 2.45.2 to 2.46.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.45.2...v2.46.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-29 09:51:59 +00:00
Jordan Sexton 5e320ba976
lending: cross collateral / cross liquidity support (#1441) 2021-04-28 20:55:33 -05:00
dependabot[bot] 636407d7a9
build(deps-dev): bump @types/node in /token-lending/js (#1642)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 15.0.0 to 15.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-28 09:41:15 +00:00
Jon Cinque 11e207cc85
stake-pool: Add user transfer authority on withdraw (#1640)
The stake pool expects pool tokens to be delegated to the withdraw
authority before performing a withdrawal. If a user delegates too many
tokens to the withdraw authority, anyone else can take the rest of their
tokens by doing their own withdrawal.

Delegate pool tokens to an ephemeral keypair and sign with that.
2021-04-27 12:53:46 +00:00
Jon Cinque 14bdbdc3ac
stake-pool: Add ability to remove a validator that has deactivating transient stake (#1624)
* Add status enum

* Add ability to remove validator with transient stake

* Only account validator stake if active

* Fix merge conflicts
2021-04-27 13:24:39 +02:00
dependabot[bot] 7d666b86ce
build(deps): bump @solana/web3.js in /token-lending/js (#1639)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.5.0 to 1.7.0.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.5.0...v1.7.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-27 09:36:21 +00:00
dependabot[bot] 81ab529311
build(deps-dev): bump @types/node in /token-lending/js (#1638)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.41 to 15.0.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-27 09:35:28 +00:00
Tyera Eulberg c9a5289aa0
Add airdrop help to token docs (#1636) 2021-04-26 20:51:43 +00:00
Jon Cinque ca6d57991a
token-swap js: Fix arg list in ts / flow (#1633) 2021-04-26 20:21:25 +00:00
Tyera Eulberg 99aaab0993
docs: specify mint in spl-token balance examples (#1634) 2021-04-26 20:20:16 +00:00
dependabot[bot] 1e28a427a4
build(deps-dev): bump eslint from 7.24.0 to 7.25.0 in /token-lending/js (#1631)
Bumps [eslint](https://github.com/eslint/eslint) from 7.24.0 to 7.25.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.24.0...v7.25.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-26 11:09:53 +00:00
Jon Cinque 61a53abf6f
stake-pool: Add ability to withdraw from reserve if no stake available (#1627) 2021-04-23 23:55:16 +02:00
Michael Vines 1e47030549 Update SPL to Solana v1.6.6 2021-04-23 13:39:51 -07:00
Trent Nelson 9ad4168253
token-cli: bump version to 2.0.11 2021-04-23 01:53:28 -06:00
Trent Nelson dfc5cc5a23 token-cli: Fix ATA creation in offline mode 2021-04-23 07:42:45 +00:00
Jon Cinque c149b0a46e
stake-pool: Add depositor key on init, required on deposit (#1616)
* stake-pool: Add depositor key on init, required on deposit

Some stake pools need to be private, and not allow outside depositors.

Enhance the existing deposit authority in the stake pool be configurable
on initialization, and then require its signature on deposit.

The existing deposit authority is a program address, making deposits
permissionless. This allows a pool creator to set their own deposit_authority on
initialization. In a great turn of events, almost everything else works
the same way!

Here's the current workflow for deposit, where the user calls
stake_program::authorize and stake_pool::deposit in the same
transaction:

* stake_program::authorize assigns staker and withdraw authority to the
  stake pool deposit authority
* stake_pool::deposit
    - uses the deposit authority to assign authority on the deposited
  stake account to the stake pool withdraw authority
    - uses the withdraw authority to merge the deposited stake into the validator stake

The deposit authority must "sign" the transaction in order to reassign
authority to the withdraw authority. Currently, as a program address, it
can just do that. With this change, if the deposit authority is set
during initialization, then that deposit authority must sign the
instruction.

There's also a little update for ease-of-use to always do the
stake_program::authorize in the same transaction as stake_pool::deposit.
This way, in case someone tries to deposit into a forbidden stake pool, the
whole transaction will bail and their stake will stay as theirs.

* Address review feedback

* Fix rebase issues
2021-04-22 21:34:41 +02:00
Jon Cinque 804a61e558
stake-pool: Increase lower limit for increase-validator-stake (#1620)
* stake-pool: Increase lower limit for increase-validator-stake

* Update test amounts in line with limit
2021-04-22 20:49:30 +02:00
Jon Cinque 3613ffe3b0
hotfix: ignore tests that break with tip of 1.6 (#1622) 2021-04-22 12:30:47 +00:00
Jon Cinque 08c4cb530a
stake-pool-cli: Add increase / decrease validator stake (#1619) 2021-04-22 00:20:35 +00:00
Jon Cinque fdba05714d
stake-pool: Add merging transient stakes in update (#1618)
* Add check for transient stake account activation on removal

* Add proper merging logic during update

* Format + clippy

* Add max possible validators

* Disallow removal for any transient stake state

* Reduce number of accounts for BPF instruction usage
2021-04-22 01:41:15 +02:00
Jon Cinque 53c86493e6
stake-pool: Add reserve stake and decrease / increase validator stake instruction (#1617)
* Add reserve stake account

* Add decrease validator stake instruction

* Cargo fmt

* Add increase instruction

* Add more increase tests

* Fix set fee tests

* Fix clippy in tests

* Add test-bpf feature to increase / decrease tests
2021-04-21 22:20:27 +02:00
Jon Cinque 40ebfc6917
stake-pool: Set fee (#1604)
* stake-pool: Add set_fee instruction

* Add more tests

* Add set-fee CLI instruction

* Update documentation

* Cargo fmt

* Re-format

* Fix clippy
2021-04-21 14:20:55 +02:00
Jon Cinque 30671aa5b3
stake-pool: Rework add / remove validator to not use pool tokens (#1581)
* Rework remove

* Add tests

* Transition to checked math

* Update CLI for new types / instructions

* Cargo fmt

* Rename voter_pubkey -> vote_account_address

* Remove max check

* Update validator balance test
2021-04-21 13:06:43 +02:00
dependabot[bot] cf8eeb0720
build(deps-dev): bump @babel/cli from 7.13.10 to 7.13.16 in /token/js (#1615)
Bumps [@babel/cli](https://github.com/babel/babel/tree/HEAD/packages/babel-cli) from 7.13.10 to 7.13.16.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.16/packages/babel-cli)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-21 08:56:11 +00:00
Jack May df994bf426
Fix float measurements (#1613)
* Fix float measurements

* add u64 divide
2021-04-21 00:33:49 +00:00
dependabot[bot] 6fee08be2f
build(deps): bump @solana/web3.js in /token-lending/js (#1612)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.4.0...v1.5.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 09:36:54 +00:00
Jack May a5c4b1e071
Switch shared-mem tests to programtest (#1611) 2021-04-19 11:14:23 -07:00
dependabot[bot] 589da55e29
build(deps): bump @solana/web3.js in /token-lending/js (#1609)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.2.7 to 1.4.0.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.2.7...v1.4.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 10:58:52 +00:00
Michael Vines ebc16782bb clippy 2021-04-18 20:27:27 -07:00
Michael Vines 18468b513f Update to Rust 1.51.0 2021-04-18 20:27:27 -07:00
dependabot[bot] 7a0e5aa14e
build(deps-dev): bump @types/node in /token-lending/js (#1603)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.37 to 14.14.41.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-16 09:34:54 +00:00
Trent Nelson 9dd807c893 docs: specify mint in `spl-token transfer` examples 2021-04-16 05:42:17 +00:00
Jon Cinque 71e5e556c4
stake-pool: Assess fee as a percentage of rewards (#1597)
* stake-pool: Collect fee every epoch as proportion of rewards

* Add more complete tests

* Update docs
2021-04-15 12:10:17 +02:00
dependabot[bot] d3e26d089b
build(deps-dev): bump @types/eslint in /token-lending/js (#1599)
Bumps [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint) from 7.2.9 to 7.2.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/eslint)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-15 09:37:10 +00:00
dependabot[bot] 48a0f81ab6
build(deps-dev): bump eslint-plugin-prettier in /token-lending/js (#1598)
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-15 09:32:54 +00:00
B 9281b6e828
feat: adjust fee constraints (#1596) 2021-04-14 13:33:34 -05:00
Trent Nelson 96901b1299 token-cli: sign_only gates for `transfer` 2021-04-14 12:13:52 -06:00
Trent Nelson b8a773fddd token-cli: sign_only gates for `authorize` 2021-04-14 12:13:52 -06:00
Trent Nelson 4374d8dee4 token-cli: Fix transfer to multisig ATAs 2021-04-14 12:13:52 -06:00
Trent Nelson 6a52ba7d92 token-cli: sign_only gates for `create-account` 2021-04-14 12:13:52 -06:00
dependabot[bot] 24ea32aa48
build(deps-dev): bump rollup from 2.45.1 to 2.45.2 in /token-lending/js (#1594)
Bumps [rollup](https://github.com/rollup/rollup) from 2.45.1 to 2.45.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.45.1...v2.45.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-14 09:36:55 +00:00
dependabot[bot] 1ff9f789e7
build(deps-dev): bump @typescript-eslint/parser in /token-lending/js (#1591)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.21.0 to 4.22.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.22.0/packages/parser)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 10:10:18 +00:00
dependabot[bot] 529b070e2b
build(deps-dev): bump @typescript-eslint/eslint-plugin (#1590)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.21.0 to 4.22.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.22.0/packages/eslint-plugin)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 09:37:18 +00:00
dependabot[bot] 13f8430b77
build(deps): bump @solana/web3.js in /token-lending/js (#1588)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.2.6 to 1.2.7.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.2.6...v1.2.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 11:33:05 +00:00
dependabot[bot] b581ab3319
build(deps-dev): bump @types/eslint in /token-lending/js (#1587)
Bumps [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint) from 7.2.8 to 7.2.9.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/eslint)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 11:30:42 +00:00
dependabot[bot] 31d5640706
build(deps-dev): bump rollup from 2.45.0 to 2.45.1 in /token-lending/js (#1585)
Bumps [rollup](https://github.com/rollup/rollup) from 2.45.0 to 2.45.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.45.0...v2.45.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 10:58:36 +00:00
dependabot[bot] 34571b624f
build(deps-dev): bump eslint from 7.23.0 to 7.24.0 in /token-lending/js (#1586)
Bumps [eslint](https://github.com/eslint/eslint) from 7.23.0 to 7.24.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.23.0...v7.24.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 10:56:42 +00:00
Jon Cinque 2b3f71ead5
stake-pool: Add rebalancing instruction interface (#1563)
* stake-pool: Add rebalancing instruction interface

* Address feedback

* Rename again

* Ignore rustdoc code
2021-04-09 12:32:21 +02:00
dependabot[bot] 2ef336fd0a
build(deps-dev): bump rollup from 2.44.0 to 2.45.0 in /token-lending/js (#1584)
Bumps [rollup](https://github.com/rollup/rollup) from 2.44.0 to 2.45.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.44.0...v2.45.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-09 09:32:01 +00:00
Tyera Eulberg 78ab468781
Add to .gitignore 2021-04-08 17:58:07 -06:00
dependabot[bot] 0d5146e30c
build(deps-dev): bump typescript in /token-lending/js (#1582)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.2.3 to 4.2.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 09:38:48 +00:00
dependabot[bot] 34583bf748
build(deps-dev): bump @typescript-eslint/eslint-plugin (#1577)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.20.0 to 4.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.21.0/packages/eslint-plugin)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-06 10:08:00 +00:00
dependabot[bot] fa5c34dc84
build(deps-dev): bump @typescript-eslint/parser in /token-lending/js (#1576)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.20.0 to 4.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.21.0/packages/parser)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-06 09:35:07 +00:00
dependabot[bot] 4d1bb013fb
build(deps-dev): bump eslint-plugin-flowtype in /token/js (#1575)
Bumps [eslint-plugin-flowtype](https://github.com/gajus/eslint-plugin-flowtype) from 5.3.1 to 5.6.0.
- [Release notes](https://github.com/gajus/eslint-plugin-flowtype/releases)
- [Commits](https://github.com/gajus/eslint-plugin-flowtype/compare/v5.3.1...v5.6.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-06 09:12:51 +00:00
Tyera Eulberg 3ab19ba514
Bump memo to v3.0.1 (#1574) 2021-04-05 14:22:05 -06:00
Jon Cinque e8f59e42ba
token-swap: Bump crate version to 2.1.0 for release (#1573) 2021-04-05 21:56:38 +02:00
Jon Cinque 7f89183c0d
stake-pool: Ensure zero pool token supply on init (#1572) 2021-04-05 20:06:40 +02:00
Michael Vines 17dd53d5e8 Break up the UpdateValidatorListBalance instructions over multiple transactions 2021-04-05 08:50:57 -07:00
Jon Cinque 8f325dcd2d
math: Improve sqrt using bit-wise operations (#1562)
* math: Improve sqrt guess using bit-wise operations

* Run fmt and bump up instruction for failed test

* Bump up compute cost from CI failure

* Update CI version of toolchain

* Address feedback
2021-04-05 14:48:27 +02:00
dependabot[bot] 0e2b08066b
build(deps): bump @solana/web3.js in /token-lending/js (#1571)
Bumps [@solana/web3.js](https://github.com/solana-labs/solana-web3.js) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/solana-labs/solana-web3.js/releases)
- [Changelog](https://github.com/solana-labs/solana-web3.js/blob/master/.releaserc.json)
- [Commits](https://github.com/solana-labs/solana-web3.js/compare/v1.2.5...v1.2.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-05 10:31:13 +00:00
Tyera Eulberg c01665832a
Ignore compute-budget test to unblock bpf-tools v1.5 (#1569) 2021-04-03 18:36:00 +00:00
Michael Vines ee52f1d499 Reimplement `spl-stake-pool list` command to use the StakePool/ValidatorList as the primary source of information 2021-04-02 16:27:42 -07:00
Jon Cinque 092432f1e1
stake-pool: Separate manager from owner (#1560)
* stake-pool: Separate manager from owner

* Add manager pubkey to stake pool
* Differentiate manager functions from owner functions
* Include a `set_manager` function to be used by the owner
* Change the term `owner` to `authority` in the CLI for clarity

* Rename manager -> staker and owner -> manager

* Split staker, manager, and token owner in CLI

* "Do not disturb the boss"
2021-04-02 10:56:12 +02:00
Jon Cinque f309df4f35
token-swap: Update package version to match crates (#1561) 2021-04-01 20:53:59 +02:00
177 changed files with 37867 additions and 16027 deletions

View File

@ -10,7 +10,34 @@ on:
- 'docs/**' - 'docs/**'
jobs: jobs:
all_github_action_checks: check_non_docs:
outputs:
run_all_github_action_checks: ${{ steps.check_files.outputs.run_all_github_action_checks }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: check modified files
id: check_files
run: |
echo "========== check paths of modified files =========="
echo "::set-output name=run_all_github_action_checks::true"
git diff --name-only HEAD^ HEAD > files.txt
while IFS= read -r file
do
if [[ $file != docs/** ]]; then
echo "Found modified non-'docs' file(s)"
echo "::set-output name=run_all_github_action_checks::false"
break
fi
done < files.txt
all_github_action_checks:
runs-on: ubuntu-latest
needs: check_non_docs
if: needs.check_non_docs.outputs.run_all_github_action_checks == 'true'
steps: steps:
- run: echo "Done" - run: echo "Done"

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ node_modules
hfuzz_target hfuzz_target
hfuzz_workspace hfuzz_workspace
**/*.so **/*.so
**/.DS_Store

1146
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,12 +11,12 @@ members = [
"feature-proposal/cli", "feature-proposal/cli",
"libraries/math", "libraries/math",
"memo/program", "memo/program",
"name-service/program",
"record/program", "record/program",
"shared-memory/program", "shared-memory/program",
"stake-pool/cli", "stake-pool/cli",
"stake-pool/program", "stake-pool/program",
"token-lending/program", "token-lending/program",
"token-lending/client",
"token-swap/program", "token-swap/program",
"token-swap/program/fuzz", "token-swap/program/fuzz",
"token/cli", "token/cli",
@ -29,3 +29,6 @@ exclude = [
"themis/program_ristretto", "themis/program_ristretto",
"token/perf-monitor", # TODO: Rework perf-monitor to use solana-program-test, avoiding the need to link directly with the BPF VM "token/perf-monitor", # TODO: Rework perf-monitor to use solana-program-test, avoiding the need to link directly with the BPF VM
] ]
[profile.dev]
split-debuginfo = "unpacked"

View File

@ -12,12 +12,12 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
solana-program = "1.6.2" solana-program = "1.6.7"
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] } spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -13,7 +13,7 @@ test-bpf = []
[dependencies] [dependencies]
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
solana-program = "1.6.2" solana-program = "1.6.7"
spl-token = { version = "3.0", path = "../../token/program", features = [ "no-entrypoint" ] } spl-token = { version = "3.0", path = "../../token/program", features = [ "no-entrypoint" ] }
thiserror = "1.0" thiserror = "1.0"
uint = "0.8" uint = "0.8"
@ -21,8 +21,8 @@ arbitrary = { version = "0.4", features = ["derive"], optional = true }
borsh = "0.8.2" borsh = "0.8.2"
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -4,13 +4,10 @@ set -ex
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
source ./ci/solana-version.sh install source ./ci/solana-version.sh install
(cd token/js && npm install)
cd token-swap/js cd token-swap/js
npm install npm install
npm run lint npm run lint
npm run flow npm run build
npx tsc module.d.ts
npm run start-with-test-validator npm run start-with-test-validator
(cd ../../target/deploy && mv spl_token_swap_production.so spl_token_swap.so) (cd ../../target/deploy && mv spl_token_swap_production.so spl_token_swap.so)
SWAP_PROGRAM_OWNER_FEE_ADDRESS="HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN" npm run start-with-test-validator SWAP_PROGRAM_OWNER_FEE_ADDRESS="HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN" npm run start-with-test-validator

View File

@ -18,13 +18,13 @@
if [[ -n $RUST_STABLE_VERSION ]]; then if [[ -n $RUST_STABLE_VERSION ]]; then
stable_version="$RUST_STABLE_VERSION" stable_version="$RUST_STABLE_VERSION"
else else
stable_version=1.50.0 stable_version=1.51.0
fi fi
if [[ -n $RUST_NIGHTLY_VERSION ]]; then if [[ -n $RUST_NIGHTLY_VERSION ]]; then
nightly_version="$RUST_NIGHTLY_VERSION" nightly_version="$RUST_NIGHTLY_VERSION"
else else
nightly_version=2021-02-18 nightly_version=2021-04-18
fi fi

View File

@ -14,7 +14,7 @@
if [[ -n $SOLANA_VERSION ]]; then if [[ -n $SOLANA_VERSION ]]; then
solana_version="$SOLANA_VERSION" solana_version="$SOLANA_VERSION"
else else
solana_version=v1.5.15 solana_version=v1.6.2
fi fi
export solana_version="$solana_version" export solana_version="$solana_version"

View File

@ -8,6 +8,7 @@ module.exports = {
"token-lending", "token-lending",
"associated-token-account", "associated-token-account",
"memo", "memo",
"name-service",
"shared-memory", "shared-memory",
"stake-pool", "stake-pool",
"feature-proposal", "feature-proposal",

57
docs/src/name-service.md Normal file
View File

@ -0,0 +1,57 @@
---
title: Name Service
---
A SPL program for issuing and managing ownership of: domain names, Solana Pubkeys, URLs, Twitter handles, ipfs cid's etc..
This program could be used for dns, pubkey etc lookups via a browser extension
for example, the goal is to create an easy way to identify Solana public keys
with various links.
Broader use cases are also imaginable.
Key points:
- A Name is a string that maps to a record (program derived account) which can hold data.
- Each name is of a certain class and has a certain owner, both are identified
by their pubkeys. The class of a name needs to sign the issuance of it.
- A name can have a parent name that is identified by the address of its record.
The owner of the parent name (when it exists) needs to sign the issuance of
the child name.
- The data of a name registry is controlled by the class keypair or, when it is
set to `Pubkey::default()`, by the name owner keypair.
- Only the owner can delete a name registry.
Remarks and use cases:
- Domain name declarations: One could arbitrarily set-up a class that we can call
Top-Level-Domain names. Names in this class can only be issued with the
permission of the class keypair, ie the administrator, who can enforce that
TLD names are of the type `".something"`. From then on one could create and
own the TLD `".sol"` and create a class of ".sol" sub-domains, administrating
the issuance of the `"something.sol"` sub-domains that way (by setting the
parent name to the address of the `".sol"` registry).
An off-chain browser extension could then, similarly to DNS, parse the user SPL
name service URL input and descend the chain of names, verifying that the names
exist with the correct parenthood, and finally use the data of the last child
name (or also a combination of the parents data) in order to resolve this call
towards a real DNS URL or any kind of data.
Although the ownership and class system makes the administration a given class
centralized, the creation of new classes is permissionless and as a class owner
any kind of decentralized governance signing program could be used.
- Twitter handles can be added as names of one specific name class. The class
authority of will therefore hold the right to add a Twitter handle name. This
enables the verification of Twitter accounts for example by asking the user to
tweet his pubkey or a signed message. A bot that holds the private issuing
authority key can then sign the Create instruction (with a metadata_authority
that is the tweeted pubkey) and send it back to the user who will then submit
it to the program.
In this case the class will still be able to control the data of the name registry, and not the user for example.
Therefore, another way of using this program would be to create a name
(`"verified-twitter-handles"` for example) with the `Pubkey::default()` class
and with the owner being the authority. That way verified Twitter names could be
issued as child names of this parent by the owner, leaving the user as being
able to modify the data of his Twitter name registry.

View File

@ -3,7 +3,7 @@ title: Stake Pool Program
--- ---
A program for pooling together SOL to be staked by an off-chain agent running A program for pooling together SOL to be staked by an off-chain agent running
a Delegation bot which redistributes the stakes across the network and tries a Delegation Bot which redistributes the stakes across the network and tries
to maximize censorship resistance and rewards. to maximize censorship resistance and rewards.
## Overview ## Overview
@ -14,7 +14,7 @@ inflation rate, total number of SOL staked on the network, and an individual
validators uptime and commission (fee). validators uptime and commission (fee).
Stake pools are an alternative method of earning staking rewards. This on-chain Stake pools are an alternative method of earning staking rewards. This on-chain
program pools together SOL to be staked by a manager, allowing SOL holders to program pools together SOL to be staked by a staker, allowing SOL holders to
stake and earn rewards without managing stakes. stake and earn rewards without managing stakes.
Additional information regarding staking and stake programming is available at: Additional information regarding staking and stake programming is available at:
@ -24,16 +24,18 @@ Additional information regarding staking and stake programming is available at:
## Motivation ## Motivation
This document is intended for stake pool managers who want to create or manage This document is intended for the main actors of the stake pool system:
stake pools, and users who want to provide staked SOL into an existing stake
pool. * manager: creates and manages the stake pool, earns fees, can update the fee, staker, and manager
* staker: adds and removes validators to the pool, rebalances stake among validators
* user: provides staked SOL into an existing stake pool
In its current iteration, the stake pool only processes totally active stakes. In its current iteration, the stake pool only processes totally active stakes.
Deposits must come from fully active stakes, and withdrawals return a fully Deposits must come from fully active stakes, and withdrawals return a fully
active stake account. active stake account.
This means that stake pool managers and users must be comfortable with creating This means that stake pool managers, stakers, and users must be comfortable with
and delegating stakes, which are more advanced operations than sending and creating and delegating stakes, which are more advanced operations than sending and
receiving SPL tokens and SOL. Additional information on stake operations are receiving SPL tokens and SOL. Additional information on stake operations are
available at: available at:
@ -46,27 +48,28 @@ like [Token Swap](token-swap.md).
## Operation ## Operation
A stake pool manager creates a stake pool and includes validators that will A stake pool manager creates a stake pool, and the staker includes validators that will
receive delegations from the pool by creating "validator stake accounts" and receive delegations from the pool by creating "validator stake accounts" and
activating a delegation on them. Once a validator stake account's delegation is activating a delegation on them. Once a validator stake account's delegation is
active, the stake pool manager adds it to the stake pool. active, the staker adds it to the stake pool.
At this point, users can participate with deposits. They must delegate a stake At this point, users can participate with deposits. They must delegate a stake
account to the one of the validators in the stake pool. Once it's active, the account to the one of the validators in the stake pool. Once it's active, the
user can deposit their stake into the pool in exchange for SPL staking derivatives user can deposit their stake into the pool in exchange for SPL staking derivatives
representing their fractional ownership in pool. A percentage of the user's representing their fractional ownership in pool. A percentage of the rewards
deposit goes to the pool manager as a fee. earned by the pool goes to the pool manager as a fee.
Over time, as the stake pool accrues staking rewards, the user's fractional Over time, as the stakes in the stake pool accrue staking rewards, the user's fractional
ownership will be worth more than their initial deposit. Whenever the user chooses, ownership will be worth more than their initial deposit. Whenever the user chooses,
they can withdraw their SPL staking derivatives in exchange for an activated stake. they can withdraw their SPL staking derivatives in exchange for an activated stake.
The stake pool manager can add and remove validators, or rebalance the pool by The stake pool staker can add and remove validators, or rebalance the pool by
withdrawing stakes from the pool, deactivating them, reactivating them on another decreasing the stake on a validator, waiting an epoch to move it into the stake
validator, then depositing back into the pool. pool's reserve account, then increasing the stake on another validator.
These manager operations require SPL staking derivatives and staked SOL, so the The staker operation to add a new validator requires roughly 1.003 SOL to create
stake pool manager will need liquidity on hand to properly manage the pool. the stake account on a validator, so the stake pool staker will need liquidity
on hand to fully manage the pool stakes.
## Background ## Background
@ -130,32 +133,105 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
solana config set --keypair usb://ledger/ solana config set --keypair usb://ledger/
``` ```
### Stake Pool Administrator Examples #### Run Locally
If you would like to test a stake pool locally without having to wait for stakes
to activate and deactivate, you can run the stake pool locally using the
`solana-test-validator` tool with shorter epochs, and pulling the current program
from devnet, testnet, or mainnet.
```sh
$ solana-test-validator -c poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj --url devnet --slots-per-epoch 32
$ solana config set --url http://127.0.0.1:8899
```
### Stake Pool Manager Examples
#### Create a stake pool #### Create a stake pool
The pool administrator manages the stake accounts in a stake pool, and in exchange The stake pool manager controls the stake pool from a high level, and in exchange
receives a fee in the form of SPL token staking derivatives. The administrator receives a fee in the form of SPL token staking derivatives. The manager
sets the fee on creation. Let's create a pool with a 3% fee: sets the fee on creation. Let's create a pool with a 3% fee and a maximum of 1000
validator stake accounts:
```sh ```sh
$ spl-stake-pool create-pool --fee-numerator 3 --fee-denominator 100 $ spl-stake-pool create-pool --fee-numerator 3 --fee-denominator 100 --max-validators 1000
Creating mint Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS Creating reserve stake 33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J
Creating pool fee collection account 3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5 Creating mint D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ
Creating stake pool 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC Creating pool fee collection account 5gpuSdutGY98KKbgmR5CfLK7toFcQD69JzKDwseegzXE
Signature: 5HdDoPssqwyLjt2QvhRbnSATZqFLGKha92zMuJiBUpKeKYKGURRV41N5ydCQxqnFjCud3xv85Z6ghErppNJzaYM8 Signature: 2dvCtHMcqxibckhvVgFQeFCRb7VcHbuFLRf71Aqd9PtzFzdbG3gAkNpxYznfpKDx2vTRrVtwW81sZAx5U3Frb5Uu
Creating stake pool EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
Signature: 2kYDVyJp8FVrLmEZyW9ivMYcXEsgWm4hFyhp5omxVtonjhYG6WS1S85sPTCdsQWe3idof6ZqsY8F3oaMXwrEkAYK
``` ```
The unique stake pool identifier is `3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC`. The unique stake pool identifier is `EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1`.
The identifier for the SPL token for staking derivatives is The identifier for the SPL token for staking derivatives is
`Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS`. The stake pool has full control `D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ`. The stake pool has full control
over the mint. over the mint.
The pool creator's fee account identifier is The pool creator's fee account identifier is
`3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5`. When users deposit warmed up `5gpuSdutGY98KKbgmR5CfLK7toFcQD69JzKDwseegzXE`. Every epoch, as stake accounts
stake accounts into the stake pool, the program will transfer 3% of their in the stake pool earn rewards, the program will mint SPL token staking derivatives
contribution into this account in the form of SPL token staking derivatives. equal to 3% of the gains on that epoch into this account. If no gains were observed,
nothing will be deposited.
The reserve stake account identifier is `33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J`.
This account holds onto additional stake used when rebalancing between validators.
For a stake pool with 1000 validators, the cost to create a stake pool is less
than 0.5 SOL.
#### Set manager
The stake pool manager may pass their administrator privileges to another account.
```sh
$ spl-stake-pool set-manager EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --new-manager 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
```
At the same time, they may also change the SPL token account that receives fees
every epoch. The mint for the provided token account must be the SPL token mint,
`D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ` in our example.
```sh
$ spl-stake-pool set-manager EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --new-fee-receiver HoCsh97wRxRXVjtG7dyfsXSwH9VxdDzC7GvAsBE1eqJz
Signature: 4aK8yzYvPBkP4PyuXTcCm529kjEH6tTt4ixc5D5ZyCrHwc4pvxAHj6wcr4cpAE1e3LddE87J1GLD466aiifcXoAY
```
#### Set fee
The stake pool manager may update the fee assessed every epoch, passing the
numerator and denominator for the fraction that make up the fee. For a fee of
10%, they could run:
```sh
$ spl-stake-pool set-fee EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 10 100
Signature: 5yPXfVj5cbKBfZiEVi2UR5bXzVDuc2c3ruBwSjkAqpvxPHigwGHiS1mXQVE4qwok5moMWT5RNYAMvkE9bnfQ1i93
```
#### Set staker
In order to manage the stake accounts, the stake pool manager or
staker can set the staker authority of the stake pool's managed accounts.
```sh
$ spl-stake-pool set-staker EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
```
Now, the new staker can perform any normal stake pool operations, including
adding and removing validators and rebalancing stake.
Important security note: the stake pool program only gives staking authority to
the pool staker and always retains withdraw authority. Therefore, a malicious
stake pool staker cannot steal funds from the stake pool.
Note: to avoid "disturbing the manager", the staker can also reassign their stake
authority.
### Stake Pool Staker Examples
#### Create a validator stake account #### Create a validator stake account
@ -170,7 +246,7 @@ lists, we choose some validators at random and start with identity
delegated to that vote account. delegated to that vote account.
```sh ```sh
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 $ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
Creating stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Creating stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
Signature: 4pA2WKT6d2wkXEtSpiQswv22WyoFad2KX6FdPEzwBiEquvaUBEtzenys5Jh1ABPCh7yc4w8kzqMRRCwDj6ZSUV1K Signature: 4pA2WKT6d2wkXEtSpiQswv22WyoFad2KX6FdPEzwBiEquvaUBEtzenys5Jh1ABPCh7yc4w8kzqMRRCwDj6ZSUV1K
``` ```
@ -179,13 +255,13 @@ In order to maximize censorship resistance, we want to distribute our SOL to as
many validators as possible, so let's add a few more. many validators as possible, so let's add a few more.
```sh ```sh
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz $ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
Creating stake account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Creating stake account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie
Signature: 4pyRZzjsWG7jP3GRZeZCo2Eb2TPjHM4kAYRFMivimme6HAee1nhzoNJBe3VSt2sv7acp5fwT7J8omBM8o3niY8gu Signature: 4pyRZzjsWG7jP3GRZeZCo2Eb2TPjHM4kAYRFMivimme6HAee1nhzoNJBe3VSt2sv7acp5fwT7J8omBM8o3niY8gu
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G $ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
Creating stake account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E Creating stake account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
Signature: 4ZUdZzUARgUCPuY8nVsJbN6vRDbVX8sYAQGYYXj2YVvjoJ2oevq2H8uzrhYApe419uoP7QYukqNstiti5p5DDukN Signature: 4ZUdZzUARgUCPuY8nVsJbN6vRDbVX8sYAQGYYXj2YVvjoJ2oevq2H8uzrhYApe419uoP7QYukqNstiti5p5DDukN
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm $ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
Creating stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Creating stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
Signature: yQqXCbuA66wQsHtkziNg3XadfZF5aCmvjfentwbZJnSPeEjJwPka3M1QY5GmR1efprptqaePn71BTMSLscX8DLr Signature: yQqXCbuA66wQsHtkziNg3XadfZF5aCmvjfentwbZJnSPeEjJwPka3M1QY5GmR1efprptqaePn71BTMSLscX8DLr
``` ```
@ -235,22 +311,21 @@ We created new validator stake accounts in the last step and staked them. Once
the stake activates, we can add them to the stake pool. the stake activates, we can add them to the stake pool.
```sh ```sh
$ spl-stake-pool add-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN $ spl-stake-pool add-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
Creating account to receive tokens Gu8xqzYFg2sPHWHhUivKNBeF9uikiauihLs9hLzziKu7
Signature: 3N1K89rGV9gWueTTrPGTDBwKAp8BikQhKHMFoREw98Q1piXFeZSSxqfnRQexrfAZQfrpYH9qwsaPWRruwkVeBivV Signature: 3N1K89rGV9gWueTTrPGTDBwKAp8BikQhKHMFoREw98Q1piXFeZSSxqfnRQexrfAZQfrpYH9qwsaPWRruwkVeBivV
``` ```
Users can start depositing their activated stakes into the stake pool, as Users can start depositing their activated stakes into the stake pool, as
long as they are delegated to the same vote account, which was long as they are delegated to the same vote account, which was
`FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` in this example. You can also `FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN` in this example. You can also
double-check that at any time using the Solana command-line utility. double-check that at any time using the Solana command-line utility.
```sh ```sh
$ solana stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN $ solana stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
Balance: 0.002282881 SOL Balance: 0.002282881 SOL
Rent Exempt Reserve: 0.00228288 SOL Rent Exempt Reserve: 0.00228288 SOL
Delegated Stake: 0.000000001 SOL Delegated Stake: 1.000000000 SOL
Active Stake: 0.000000001 SOL Active Stake: 1.000000000 SOL
Activating Stake: 0 SOL Activating Stake: 0 SOL
Stake activates starting from epoch: 161 Stake activates starting from epoch: 161
Delegated Vote Account Address: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 Delegated Vote Account Address: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
@ -260,26 +335,31 @@ Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
#### Remove validator stake account #### Remove validator stake account
If the stake pool manager wants to stop delegating to a vote account, they can If the stake pool staker wants to stop delegating to a vote account, they can
totally remove the validator stake account from the stake pool by providing totally remove the validator stake account from the stake pool.
staking derivatives, just like `withdraw`.
```sh ```sh
$ spl-stake-pool remove-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF $ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
``` ```
The difference with `withdraw` is that the validator stake account is totally The difference with `withdraw` is that the validator stake account is totally
removed from the stake pool and now belongs to the administrator. removed from the stake pool and now belongs to the administrator. The authority
for the withdrawn stake account can also be specified using the `--new-authority` flag:
```sh
$ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G --new-authority 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
```
We can check the removed stake account: We can check the removed stake account:
```sh ```sh
$ solana stake-account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E $ solana stake-account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
Balance: 1.002282881 SOL Balance: 1.002282880 SOL
Rent Exempt Reserve: 0.00228288 SOL Rent Exempt Reserve: 0.00228288 SOL
Delegated Stake: 1.000000001 SOL Delegated Stake: 1.000000000 SOL
Active Stake: 1.000000001 SOL Active Stake: 1.000000000 SOL
Delegated Vote Account Address: AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G Delegated Vote Account Address: AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
@ -291,7 +371,7 @@ removal of staked SOL from the pool.
We can also double-check that the stake pool no longer shows the stake account: We can also double-check that the stake pool no longer shows the stake account:
```sh ```sh
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC $ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881 Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673 Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652 Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
@ -300,14 +380,14 @@ Total: ◎15.849959206
#### Rebalance the stake pool #### Rebalance the stake pool
As time goes on, deposits and withdrawals will happen to all of the stake accounts As time goes on, users will deposit to and withdraw from all of the stake accounts
managed by the pool, and the stake pool manager may want to rebalance the stakes. managed by the pool, and the stake pool staker may want to rebalance the stakes.
For example, let's say the manager wants the same delegation to every validator For example, let's say the staker wants the same delegation to every validator
in the pool. When they look at the state of the pool, they see: in the pool. When they look at the state of the pool, they see:
```sh ```sh
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC $ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881 Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673 Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652 Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
@ -315,75 +395,63 @@ Total: ◎15.849959206
``` ```
This isn't great! The last stake account, `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie` This isn't great! The last stake account, `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`
has too much allocated. For their strategy, the manager wants the `15.849959206` has too much allocated. For their strategy, the staker wants the `15.849959206`
SOL to be distributed evenly, meaning around `5.283319735` in each account. They need SOL to be distributed evenly, meaning around `5.283319735` in each account. They need
to move `4.281036854` to `FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` and to move `4.281036854` to `FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` and
`1.872447062` to `FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN`. `1.872447062` to `FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN`.
First, they need to withdraw a total of `6.153483916` from ##### Decrease validator stake
`E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`. Using the `spl-token` utility,
let's check the total supply of pool tokens: First, they need to decrease the amount on stake account
`E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`, delegated to
`HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz`, by total of `6.153483916` SOL.
They decrease that amount of SOL:
```sh ```sh
$ spl-token supply Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS $ spl-stake-pool decrease-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz 6.153483916
0.034692168 Signature: ZpQGwT85rJ8Y9afdkXhKo3TVv4xgTz741mmZj2vW7mihYseAkFsazWxza2y8eNGY4HDJm15c1cStwyiQzaM3RpH
``` ```
Given a total pool token supply of `0.034692168` and total staked SOL amount of Internally, this instruction splits and deactivates 6.153483916 SOL from the
`15.849959206`, let's calculate how many pool tokens to withdraw from the pool: validator stake account `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie` into a
transient stake account, owned and managed entirely by the stake pool.
``` Once the stake is deactivated during the next epoch, the `update` command will
sol_to_withdraw * total_pool_tokens / total_sol_staked = pool_tokens_to_withdraw automatically merge the transient stake account into a reserve stake account,
6.153483916 * 0.034692168 / 15.849959206 ~ 0.013468659 also entirely owned and managed by the stake pool.
```
They withdraw that amount of pool tokens: ##### Increase validator stake
Now that the reserve stake account has enough to perform the rebalance, the staker
can increase the stake on the two other validators,
`8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm` and
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`.
They add 4.281036854 SOL to `8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm`:
```sh ```sh
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.013468659 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF $ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm 4.281036854
Withdrawing from account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie, amount ◎6.153483855, 0.013468659 pool tokens Signature: 3GJACzjUGLPjcd9RLUW86AfBLWKapZRkxnEMc2yHT6erYtcKBgCapzyrVH6VN8Utxj7e2mtvzcigwLm6ZafXyTMw
Creating account to receive stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk
Signature: z8a5ZRfWdj8Fcsr3ttCJ731wFKyhZNcqoKEdV1RBCkzr3tHGQNCC56qvRVJ6oxyCVDqWZ3KL1Bkyn3sDpjYPDku
``` ```
Because of rounding in the calculation a few lines above, it looks like we receive And they add 1.872447062 SOL to `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`:
less than we should. If we play that back the other way, we'll see that all is well:
```
pool_tokens_to_withdraw * total_sol_staked / total_pool_tokens = sol_to_withdraw
0.013468659 * 15.849959206 / 0.034692168 ~ 6.153483855
```
Next, they deactivate the new received stake:
```sh ```sh
$ solana deactivate-stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk $ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 1.872447062
Signature: 4SuwZK5JvYkYVkM5yfu2x8x6iou6558teMwzphGECLmstMVoWbSvngUH48Ra24PrxtgUDyVDA8SXYS1qMyx3fjMj Signature: 4zaKYu3MQ3as8reLbuHKaXN8FNaHvpHuiZtsJeARo67UKMo6wUUoWE88Fy8N4EYQYicuwULTNffcUD3a9jY88PoU
``` ```
Once the stake is deactivated during the next epoch, they split the stake Internally, this instruction also uses transient stake accounts. This time, the
and activate it on the other two validator vote accounts. For brevity, those stake pool splits from the reserve stake, into the transient stake account,
commands are omitted. then activates it to the appropriate validator.
Eventually, we are left with stake account `4zppED2kFodUS2hBf8Fzeepu6yZ6QuyeNPBXCT9VU6fK` One to two epochs later, once the transient stakes activate, the `update` command
with `4.281036854` delegated to `8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm` automatically merges the transient stakes into the validator stake account, leaving
and stake account `GCJnuFGCDzaToPwJtG5GiK4g3DJBfuhQy6388NyGcfwf` with `1.872447062` a fully rebalanced stake pool:
delegated to `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`.
Once the new stakes are ready, the manager deposits them back into the stake pool:
```sh
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC GCJnuFGCDzaToPwJtG5GiK4g3DJBfuhQy6388NyGcfwf --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
Signature: jKsdEr3zxF2zZs78rmrP3PmQiTwE7v15ieEuxp4db1VQe9owXVGM8nM3dJqVRHXPsS4frQW4gJ6xBfTTk2HvKDX
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4zppED2kFodUS2hBf8Fzeepu6yZ6QuyeNPBXCT9VU6fK --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
Depositing into stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
Signature: 3JXvTvea6F4Epd2krSxnTRZPB4gLZ8GqisFE58Z4ocV92fDN1HRMVPoPhJtYcfuF12vyQZUueKwVmkvL6Wgf2evc
```
Leaving them with a rebalanced stake pool!
```sh ```sh
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC $ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎5.283340235 Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎5.283340235
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎5.283612231 Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎5.283612231
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎5.284317422 Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎5.284317422
@ -391,33 +459,7 @@ Total: ◎15.851269888
``` ```
Due to staking rewards that accrued during the rebalancing process, the pool is Due to staking rewards that accrued during the rebalancing process, the pool is
not prefectly balanced. This is completely normal. not perfectly balanced. This is completely normal.
#### Set staking authority
In order to manage the stake accounts more directly, the stake pool owner can
set the stake authority of the stake pool's managed accounts.
```sh
$ spl-stake-pool set-staking-auth 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN --new-staker 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
```
Now, the new staking authority can perform any normal staking operations,
including deactivating or re-staking.
Important security note: the stake pool program only gives staking authority to
the pool owner and always retains withdraw authority. Therefore, a malicious
stake pool manager cannot steal funds from the stake pool.
#### Set owner
The stake pool owner may pass their administrator privileges to another account.
```sh
$ spl-stake-pool 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --new-owner 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
```
### User Examples ### User Examples
@ -429,7 +471,7 @@ command-line utility has a special instruction for finding out which vote
accounts are already associated with the stake pool. accounts are already associated with the stake pool.
```sh ```sh
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC $ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E 1.002282880 SOL CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E 1.002282880 SOL
E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie 1.002282880 SOL E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie 1.002282880 SOL
FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN 1.002282880 SOL FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN 1.002282880 SOL
@ -441,13 +483,13 @@ If the manager has recently created the stake pool, and there are no stake
accounts present yet, the command-line utility will inform us. accounts present yet, the command-line utility will inform us.
```sh ```sh
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC $ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
No accounts found. No accounts found.
``` ```
#### Deposit stake #### Deposit stake
Stake pools only accept deposits from fully staked accounts, so we must first Stake pools only accept deposits from active accounts, so we must first
create stake accounts and delegate them to one of the validators managed by the create stake accounts and delegate them to one of the validators managed by the
stake pool. Using the `list` command from the previous section, we see that stake pool. Using the `list` command from the previous section, we see that
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3` is a valid vote account, so let's `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3` is a valid vote account, so let's
@ -473,17 +515,19 @@ Two epochs later, when the stake is fully active and has received one epoch of
rewards, we can deposit the stake into the stake pool. rewards, we can deposit the stake into the stake pool.
```sh ```sh
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa $ spl-stake-pool deposit EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
Creating account to receive tokens 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF Creating account to receive tokens 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
``` ```
The CLI will default to using the fee payer's
[Associated Token Account](associated-token-account.md) for stake pool tokens.
Alternatively, you can create an SPL token account yourself and pass it as the Alternatively, you can create an SPL token account yourself and pass it as the
`token-receiver` for the command. `token-receiver` for the command.
```sh ```sh
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF $ spl-stake-pool deposit EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
``` ```
@ -505,7 +549,8 @@ In order to calculate the proper value of these stake pool tokens, we must updat
the total value managed by the stake pool every epoch. the total value managed by the stake pool every epoch.
```sh ```sh
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC $ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
Updating stake pool...
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
``` ```
@ -513,13 +558,33 @@ If another user already updated the stake pool balance for the current epoch, we
see a different output. see a different output.
```sh ```sh
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC $ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
Stake pool balances are up to date, no update required. Update not required
``` ```
If no one updates the stake pool in the current epoch, the deposit and withdraw If no one updates the stake pool in the current epoch, the deposit and withdraw
instructions will fail. The update instruction is permissionless, so any user instructions will fail. The update instruction is permissionless, so any user
can run it before depositing or withdrawing. can run it before depositing or withdrawing. As a convenience, the CLI attempts
to update before running any instruction on the stake pool.
If the stake pool transient stakes are in an unexpected state, and merges are
not possible, there is the option to only update the stake pool balances without
performing merges using the `--no-merge` flag.
```sh
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --no-merge
Updating stake pool...
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
```
Later on, whenever the transient stakes are ready to be merged, it is possible to
force another update in the same epoch using the `--force` flag.
```sh
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --force
Updating stake pool...
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
```
#### Withdraw stake #### Withdraw stake
@ -529,7 +594,7 @@ staking derivative SPL tokens in exchange for an activated stake account.
Let's withdraw 0.02 staking derivative tokens from the stake pool. Let's withdraw 0.02 staking derivative tokens from the stake pool.
```sh ```sh
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF $ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
@ -550,15 +615,58 @@ Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
``` ```
Alternatively, the user can specify an existing stake account to receive their Alternatively, the user can specify an existing uninitialized stake account to
stake using the `stake-receiver` parameter. receive their stake using the `--stake-receiver` parameter.
```sh ```sh
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ $ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
``` ```
By default, the withdraw command uses the fee payer's associated token account to
source the derivative tokens. It's possible to specify the SPL token account using
the `--pool-account` flag.
```sh
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --pool-account 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
```
By default, the withdraw command will withdraw from the largest validator stake
accounts in the pool. It's also possible to specify a specific vote account for
the withdraw using the `--vote-account` flag.
```sh
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --vote-account 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
```
Note that the associated validator stake account must have enough lamports to
satisfy the pool token amount requested.
##### Special case: exiting pool with a delinquent staker
With the reserve stake, it's possible for a delinquent or malicious staker to
move all stake into the reserve through `decrease-validator-stake`, so the
staking derivatives will not gain rewards, and the stake pool users will not
be able to withdraw their funds.
To get around this case, it is also possible to withdraw from the stake pool's
reserve, but only if all of the validator stake accounts are at the minimum amount of
`1 SOL + stake account rent exemption`.
```sh
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --use-reserve
Withdrawing from account 33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J, amount 8.867176377 SOL, 0.02 pool tokens
Creating account to receive stake 9E5YzXXu9NDhtMxWJKCwe2M8Sdz6vL6bcBS92U76PVtE
Signature: 4aZaeT9Azcq23PdKcjbQLseNveZVAQ4xMabBGQspfX316cE62Q2hoES373ExbT9y2JUhug7SgdybNaCjuZ6uqNYf
```
## Appendix ## Appendix
### Activated stakes ### Activated stakes
@ -569,6 +677,22 @@ are not equivalent to inactive, activating, or deactivating stakes due to the
time cost of staking. Otherwise, malicious actors can deposit stake in one state time cost of staking. Otherwise, malicious actors can deposit stake in one state
and withdraw it in another state without waiting. and withdraw it in another state without waiting.
### Transient stake accounts
Each validator gets one transient stake account, so the staker can only
perform one action at a time on a validator. It's impossible to increase
and decrease the stake on a validator at the same time. The staker must wait for
the existing transient stake account to get merged during an `update` instruction
before performing a new action.
### Reserve stake account
Every stake pool is initialized with an undelegated reserve stake account, used
to hold undelegated stake in process of rebalancing. After the staker decreases
the stake on a validator, one epoch later, the update operation will merge the
decreased stake into the reserve. Conversely, whenever the staker increases the
stake on a validator, the lamports are drawn from the reserve stake account.
### Staking Credits Observed on Deposit ### Staking Credits Observed on Deposit
A deposited stake account's "credits observed" must match the destination A deposited stake account's "credits observed" must match the destination

View File

@ -83,6 +83,15 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
solana config set --keypair usb://ledger/ solana config set --keypair usb://ledger/
``` ```
#### Airdrop SOL
Creating tokens and accounts requires SOL for account rent deposits and
transaction fees. If the cluster you are targeting offers a faucet, you can get
a little SOL for testing:
```
solana airdrop 1
```
### Example: Creating your own fungible token ### Example: Creating your own fungible token
```sh ```sh
@ -109,7 +118,7 @@ Signature: 42Sa5eK9dMEQyvD9GMHuKxXf55WLZ7tfjabUKDhNoZRAxj9MsnN7omriWMEHXLea3aYpj
`7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi` is now an empty account: `7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi` is now an empty account:
```sh ```sh
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi $ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
0 0
``` ```
@ -126,7 +135,7 @@ The token `supply` and account `balance` now reflect the result of minting:
```sh ```sh
$ spl-token supply AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM $ spl-token supply AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
100 100
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi $ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
100 100
``` ```
@ -166,7 +175,7 @@ address by running `solana address` and provides it to the sender.
The sender then runs: The sender then runs:
``` ```
$ spl-token transfer 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg $ spl-token transfer AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
Transfer 50 tokens Transfer 50 tokens
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
@ -184,7 +193,7 @@ The receiver obtains their wallet address by running `solana address` and provid
The sender then runs to fund the receiver's associated token account, at the The sender then runs to fund the receiver's associated token account, at the
sender's expense, and then transfers 50 tokens into it: sender's expense, and then transfers 50 tokens into it:
``` ```
$ spl-token transfer --fund-recipient 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg $ spl-token transfer --fund-recipient AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
Transfer 50 tokens Transfer 50 tokens
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
@ -228,9 +237,9 @@ CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe
### Example: Create a non-fungible token ### Example: Create a non-fungible token
Create the token type, Create the token type with nine decimal places,
``` ```
$ spl-token create-token $ spl-token create-token --decimals 9
Creating token 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z Creating token 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
Signature: 4kz82JUey1B9ki1McPW7NYv1NqPKCod6WNptSkYqtuiEsQb9exHaktSAHJJsm4YxuGNW4NugPJMFX9ee6WA2dXts Signature: 4kz82JUey1B9ki1McPW7NYv1NqPKCod6WNptSkYqtuiEsQb9exHaktSAHJJsm4YxuGNW4NugPJMFX9ee6WA2dXts
``` ```
@ -264,7 +273,7 @@ Now the `7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM` account holds the
one and only `559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z` token: one and only `559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z` token:
``` ```
$ spl-token account-info 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM $ spl-token account-info 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
Address: 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM Address: 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM
Balance: 1 Balance: 1
@ -520,7 +529,7 @@ There is a rich set of JSON RPC methods available for use with SPL Token:
See https://docs.solana.com/apps/jsonrpc-api for more details. See https://docs.solana.com/apps/jsonrpc-api for more details.
Additionally the versatile `getProgramAcccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest. Additionally the versatile `getProgramAccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
### Finding all token accounts for a specific mint ### Finding all token accounts for a specific mint
@ -553,7 +562,7 @@ curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/js
``` ```
The `"dataSize": 165` filter selects all [Token The `"dataSize": 165` filter selects all [Token
Acccount](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s, Account](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
and then the `"memcmp": ...` filter selects based on the and then the `"memcmp": ...` filter selects based on the
[mint](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L88) [mint](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L88)
address within each token account. address within each token account.
@ -588,7 +597,7 @@ curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/js
``` ```
The `"dataSize": 165` filter selects all [Token The `"dataSize": 165` filter selects all [Token
Acccount](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s, Account](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
and then the `"memcmp": ...` filter selects based on the and then the `"memcmp": ...` filter selects based on the
[owner](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L90) [owner](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L90)
address within each token account. address within each token account.
@ -852,3 +861,13 @@ the maximum allowed transaction size, remove those extra clean up instructions.
They can be cleaned up during the next send operation. They can be cleaned up during the next send operation.
The `spl-token gc` command provides an example implementation of this cleanup process. The `spl-token gc` command provides an example implementation of this cleanup process.
### Token Vesting Contract:
This program allows you to lock arbitrary SPL tokens and release the locked tokens with a determined unlock schedule. An `unlock schedule` is made of a `unix timestamp` and a token `amount`, when initializing a vesting contract, the creator can pass an array of `unlock schedule` with an arbitrary size giving the creator of the contract complete control of how the tokens unlock over time.
Unlocking works by pushing a permissionless crank on the contract that moves the tokens to the pre-specified address. The recipient address of a vesting contract can be modified by the owner of the current recipient key, meaning that vesting contract locked tokens can be traded.
- Code: [https://github.com/Bonfida/token-vesting](https://github.com/Bonfida/token-vesting)
- UI: [https://vesting.bonfida.com/#/](https://vesting.bonfida.com/#/)
- Audit: The audit was conducted by Kudelski, the report can be found [here](https://github.com/Bonfida/token-vesting/blob/master/audit/Bonfida_SecurityAssessment_Vesting_Final050521.pdf)

View File

@ -22,7 +22,7 @@ extern uint64_t do_invoke(SolParameters *params) {
const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}}; const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}};
SolPubkey expected_allocated_key; SolPubkey expected_allocated_key;
if (SUCCESS == sol_create_program_address(seeds, SOL_ARRAY_SIZE(seeds), if (SUCCESS != sol_create_program_address(seeds, SOL_ARRAY_SIZE(seeds),
params->program_id, params->program_id,
&expected_allocated_key)) { &expected_allocated_key)) {
return ERROR_INVALID_INSTRUCTION_DATA; return ERROR_INVALID_INSTRUCTION_DATA;
@ -31,8 +31,7 @@ extern uint64_t do_invoke(SolParameters *params) {
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
SolAccountMeta arguments[] = {{system_program_info->key, false, false}, SolAccountMeta arguments[] = {{allocated_info->key, true, true}};
{allocated_info->key, true, true}};
uint8_t data[4 + 8]; // Enough room for the Allocate instruction uint8_t data[4 + 8]; // Enough room for the Allocate instruction
*(uint16_t *)data = 8; // Allocate instruction enum value *(uint16_t *)data = 8; // Allocate instruction enum value
*(uint64_t *)(data + 4) = SIZE; // Size to allocate *(uint64_t *)(data + 4) = SIZE; // Size to allocate

View File

@ -13,11 +13,11 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
solana-program = "1.6.2" solana-program = "1.6.7"
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -15,11 +15,11 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
solana-program = "1.6.2" solana-program = "1.6.7"
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -13,11 +13,11 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
solana-program = "1.6.2" solana-program = "1.6.7"
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -13,11 +13,11 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
solana-program = "1.6.2" solana-program = "1.6.7"
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -12,11 +12,11 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
solana-program = "1.6.2" solana-program = "1.6.7"
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -10,11 +10,11 @@ edition = "2018"
[dependencies] [dependencies]
chrono = "0.4.19" chrono = "0.4.19"
clap = "2.33.3" clap = "2.33.3"
solana-clap-utils = "1.6.2" solana-clap-utils = "1.6.7"
solana-cli-config = "1.6.2" solana-cli-config = "1.6.7"
solana-client = "1.6.2" solana-client = "1.6.7"
solana-logger = "1.6.2" solana-logger = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
spl-feature-proposal = { version = "1.0", path = "../program", features = ["no-entrypoint"] } spl-feature-proposal = { version = "1.0", path = "../program", features = ["no-entrypoint"] }
[[bin]] [[bin]]

View File

@ -12,15 +12,15 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
borsh = "0.7.1" borsh = "0.8"
borsh-derive = "0.8.1" borsh-derive = "0.8.1"
solana-program = "1.6.2" solana-program = "1.6.7"
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] } spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
[dev-dependencies] [dev-dependencies]
futures = "0.3" futures = "0.3"
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -155,13 +155,12 @@ pub fn tally(feature_proposal_address: &Pubkey) -> Instruction {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::borsh_utils;
#[test] #[test]
fn test_get_packed_len() { fn test_get_packed_len() {
assert_eq!( assert_eq!(
FeatureProposalInstruction::get_packed_len(), FeatureProposalInstruction::get_packed_len(),
borsh_utils::get_packed_len::<FeatureProposalInstruction>() solana_program::borsh::get_packed_len::<FeatureProposalInstruction>()
) )
} }

View File

@ -2,7 +2,6 @@
#![deny(missing_docs)] #![deny(missing_docs)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
pub mod borsh_utils;
mod entrypoint; mod entrypoint;
pub mod instruction; pub mod instruction;
pub mod processor; pub mod processor;

View File

@ -59,13 +59,12 @@ impl Pack for FeatureProposal {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::borsh_utils;
#[test] #[test]
fn test_get_packed_len() { fn test_get_packed_len() {
assert_eq!( assert_eq!(
FeatureProposal::get_packed_len(), FeatureProposal::get_packed_len(),
borsh_utils::get_packed_len::<FeatureProposal>() solana_program::borsh::get_packed_len::<FeatureProposal>()
); );
} }

View File

@ -68,13 +68,13 @@ async fn test_basic() {
banks_client.process_transaction(transaction).await.unwrap(); banks_client.process_transaction(transaction).await.unwrap();
// Confirm feature id account is now funded and allocated, but not assigned // Confirm feature id account is now funded and allocated, but not assigned
let feature_id_acccount = banks_client let feature_id_account = banks_client
.get_account(feature_id_address) .get_account(feature_id_address)
.await .await
.expect("success") .expect("success")
.expect("some account"); .expect("some account");
assert_eq!(feature_id_acccount.owner, system_program::id()); assert_eq!(feature_id_account.owner, system_program::id());
assert_eq!(feature_id_acccount.data.len(), Feature::size_of()); assert_eq!(feature_id_account.data.len(), Feature::size_of());
// Confirm mint account state // Confirm mint account state
let mint = get_account_data::<spl_token::state::Mint>(&mut banks_client, mint_address) let mint = get_account_data::<spl_token::state::Mint>(&mut banks_client, mint_address)
@ -115,12 +115,12 @@ async fn test_basic() {
banks_client.process_transaction(transaction).await.unwrap(); banks_client.process_transaction(transaction).await.unwrap();
// Confirm feature id account is not yet assigned // Confirm feature id account is not yet assigned
let feature_id_acccount = banks_client let feature_id_account = banks_client
.get_account(feature_id_address) .get_account(feature_id_address)
.await .await
.expect("success") .expect("success")
.expect("some account"); .expect("some account");
assert_eq!(feature_id_acccount.owner, system_program::id()); assert_eq!(feature_id_account.owner, system_program::id());
assert!(matches!( assert!(matches!(
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await, get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
@ -158,12 +158,12 @@ async fn test_basic() {
banks_client.process_transaction(transaction).await.unwrap(); banks_client.process_transaction(transaction).await.unwrap();
// Confirm feature id account is now assigned // Confirm feature id account is now assigned
let feature_id_acccount = banks_client let feature_id_account = banks_client
.get_account(feature_id_address) .get_account(feature_id_address)
.await .await
.expect("success") .expect("success")
.expect("some account"); .expect("some account");
assert_eq!(feature_id_acccount.owner, feature::id()); assert_eq!(feature_id_account.owner, feature::id());
// Confirm feature proposal account state // Confirm feature proposal account state
assert!(matches!( assert!(matches!(

View File

@ -12,18 +12,18 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
borsh = "0.7.1" borsh = "0.8"
borsh-derive = "0.8.1" borsh-derive = "0.8.1"
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
solana-program = "1.6.2" solana-program = "1.6.7"
thiserror = "1.0" thiserror = "1.0"
uint = "0.8" uint = "0.8"
[dev-dependencies] [dev-dependencies]
proptest = "0.10" proptest = "0.10"
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -1,35 +1,42 @@
//! Approximation calculations //! Approximation calculations
use { use {
num_traits::{CheckedAdd, CheckedDiv, One, Zero}, num_traits::{CheckedShl, CheckedShr, PrimInt},
std::cmp::Eq, std::cmp::Ordering,
}; };
const SQRT_ITERATIONS: u8 = 50; /// Calculate square root of the given number
///
/// Code lovingly adapted from the excellent work at:
/// https://github.com/derekdreery/integer-sqrt-rs
///
/// The algorithm is based on the implementation in:
/// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_(base_2)
pub fn sqrt<T: PrimInt + CheckedShl + CheckedShr>(radicand: T) -> Option<T> {
match radicand.cmp(&T::zero()) {
Ordering::Less => return None, // fail for less than 0
Ordering::Equal => return Some(T::zero()), // do nothing for 0
_ => {}
}
/// Perform square root // Compute bit, the largest power of 4 <= n
pub fn sqrt<T: CheckedAdd + CheckedDiv + One + Zero + Eq + Copy>(radicand: T) -> Option<T> { let max_shift: u32 = T::zero().leading_zeros() - 1;
if radicand == T::zero() { let shift: u32 = (max_shift - radicand.leading_zeros()) & !1;
return Some(T::zero()); let mut bit = T::one().checked_shl(shift)?;
}
// A good initial guess is the average of the interval that contains the let mut n = radicand;
// input number. For all numbers, that will be between 1 and the given number. let mut result = T::zero();
let one = T::one(); while bit != T::zero() {
let two = one.checked_add(&one)?; let result_with_bit = result.checked_add(&bit)?;
let mut guess = radicand.checked_div(&two)?.checked_add(&one)?; if n >= result_with_bit {
let mut last_guess = guess; n = n.checked_sub(&result_with_bit)?;
for _ in 0..SQRT_ITERATIONS { result = result.checked_shr(1)?.checked_add(&bit)?;
// x_k+1 = (x_k + radicand / x_k) / 2
guess = last_guess
.checked_add(&radicand.checked_div(&last_guess)?)?
.checked_div(&two)?;
if last_guess == guess {
break;
} else { } else {
last_guess = guess; result = result.checked_shr(1)?;
} }
bit = bit.checked_shr(2)?;
} }
Some(guess) Some(result)
} }
#[cfg(test)] #[cfg(test)]

View File

@ -43,7 +43,16 @@ pub enum MathInstruction {
/// The multipier /// The multipier
multiplier: u64, multiplier: u64,
}, },
/// Multiply two float valies /// Divide two u64 values
///
/// No accounts required for this instruction
U64Divide {
/// The dividend
dividend: u64,
/// The divisor
divisor: u64,
},
/// Multiply two float values
/// ///
/// No accounts required for this instruction /// No accounts required for this instruction
F32Multiply { F32Multiply {
@ -52,7 +61,7 @@ pub enum MathInstruction {
/// The multipier /// The multipier
multiplier: f32, multiplier: f32,
}, },
/// Divide two float valies /// Divide two float values
/// ///
/// No accounts required for this instruction /// No accounts required for this instruction
F32Divide { F32Divide {
@ -114,6 +123,17 @@ pub fn u64_multiply(multiplicand: u64, multiplier: u64) -> Instruction {
} }
} }
/// Create PreciseSquareRoot instruction
pub fn u64_divide(dividend: u64, divisor: u64) -> Instruction {
Instruction {
program_id: id(),
accounts: vec![],
data: MathInstruction::U64Divide { dividend, divisor }
.try_to_vec()
.unwrap(),
}
}
/// Create PreciseSquareRoot instruction /// Create PreciseSquareRoot instruction
pub fn f32_multiply(multiplicand: f32, multiplier: f32) -> Instruction { pub fn f32_multiply(multiplicand: f32, multiplier: f32) -> Instruction {
Instruction { Instruction {

View File

@ -3,9 +3,36 @@
use { use {
crate::{approximations::sqrt, instruction::MathInstruction, precise_number::PreciseNumber}, crate::{approximations::sqrt, instruction::MathInstruction, precise_number::PreciseNumber},
borsh::BorshDeserialize, borsh::BorshDeserialize,
solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}, solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, log::sol_log_compute_units, msg,
pubkey::Pubkey,
},
}; };
/// u64_multiply
#[inline(never)]
fn u64_multiply(multiplicand: u64, multiplier: u64) -> u64 {
multiplicand * multiplier
}
/// u64_divide
#[inline(never)]
fn u64_divide(dividend: u64, divisor: u64) -> u64 {
dividend / divisor
}
/// f32_multiply
#[inline(never)]
fn f32_multiply(multiplicand: f32, multiplier: f32) -> f32 {
multiplicand * multiplier
}
/// f32_divide
#[inline(never)]
fn f32_divide(dividend: f32, divisor: f32) -> f32 {
dividend / divisor
}
/// Instruction processor /// Instruction processor
pub fn process_instruction( pub fn process_instruction(
_program_id: &Pubkey, _program_id: &Pubkey,
@ -17,19 +44,25 @@ pub fn process_instruction(
MathInstruction::PreciseSquareRoot { radicand } => { MathInstruction::PreciseSquareRoot { radicand } => {
msg!("Calculating square root using PreciseNumber"); msg!("Calculating square root using PreciseNumber");
let radicand = PreciseNumber::new(radicand as u128).unwrap(); let radicand = PreciseNumber::new(radicand as u128).unwrap();
sol_log_compute_units();
let result = radicand.sqrt().unwrap().to_imprecise().unwrap() as u64; let result = radicand.sqrt().unwrap().to_imprecise().unwrap() as u64;
sol_log_compute_units();
msg!("{}", result); msg!("{}", result);
Ok(()) Ok(())
} }
MathInstruction::SquareRootU64 { radicand } => { MathInstruction::SquareRootU64 { radicand } => {
msg!("Calculating u64 square root"); msg!("Calculating u64 square root");
sol_log_compute_units();
let result = sqrt(radicand).unwrap(); let result = sqrt(radicand).unwrap();
sol_log_compute_units();
msg!("{}", result); msg!("{}", result);
Ok(()) Ok(())
} }
MathInstruction::SquareRootU128 { radicand } => { MathInstruction::SquareRootU128 { radicand } => {
msg!("Calculating u128 square root"); msg!("Calculating u128 square root");
sol_log_compute_units();
let result = sqrt(radicand).unwrap(); let result = sqrt(radicand).unwrap();
sol_log_compute_units();
msg!("{}", result); msg!("{}", result);
Ok(()) Ok(())
} }
@ -38,7 +71,17 @@ pub fn process_instruction(
multiplier, multiplier,
} => { } => {
msg!("Calculating U64 Multiply"); msg!("Calculating U64 Multiply");
let result = multiplicand * multiplier; sol_log_compute_units();
let result = u64_multiply(multiplicand, multiplier);
sol_log_compute_units();
msg!("{}", result);
Ok(())
}
MathInstruction::U64Divide { dividend, divisor } => {
msg!("Calculating U64 Divide");
sol_log_compute_units();
let result = u64_divide(dividend, divisor);
sol_log_compute_units();
msg!("{}", result); msg!("{}", result);
Ok(()) Ok(())
} }
@ -47,13 +90,17 @@ pub fn process_instruction(
multiplier, multiplier,
} => { } => {
msg!("Calculating f32 Multiply"); msg!("Calculating f32 Multiply");
let result = multiplicand * multiplier; sol_log_compute_units();
let result = f32_multiply(multiplicand, multiplier);
sol_log_compute_units();
msg!("{}", result as u64); msg!("{}", result as u64);
Ok(()) Ok(())
} }
MathInstruction::F32Divide { dividend, divisor } => { MathInstruction::F32Divide { dividend, divisor } => {
msg!("Calculating f32 Divide"); msg!("Calculating f32 Divide");
let result = dividend / divisor; sol_log_compute_units();
let result = f32_divide(dividend, divisor);
sol_log_compute_units();
msg!("{}", result as u64); msg!("{}", result as u64);
Ok(()) Ok(())
} }

View File

@ -62,7 +62,7 @@ async fn test_sqrt_u128() {
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
// Dial down the BPF compute budget to detect if the operation gets bloated in the future // Dial down the BPF compute budget to detect if the operation gets bloated in the future
pc.set_bpf_compute_max_units(5_500); pc.set_bpf_compute_max_units(4_000);
let (mut banks_client, payer, recent_blockhash) = pc.start().await; let (mut banks_client, payer, recent_blockhash) = pc.start().await;
@ -78,8 +78,7 @@ async fn test_sqrt_u128() {
async fn test_sqrt_u128_max() { async fn test_sqrt_u128_max() {
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
// This is pretty big too! pc.set_bpf_compute_max_units(6_000);
pc.set_bpf_compute_max_units(90_000);
let (mut banks_client, payer, recent_blockhash) = pc.start().await; let (mut banks_client, payer, recent_blockhash) = pc.start().await;
@ -103,6 +102,20 @@ async fn test_u64_multiply() {
banks_client.process_transaction(transaction).await.unwrap(); banks_client.process_transaction(transaction).await.unwrap();
} }
#[tokio::test]
async fn test_u64_divide() {
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
pc.set_bpf_compute_max_units(1650);
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
let mut transaction =
Transaction::new_with_payer(&[instruction::u64_divide(3, 1)], Some(&payer.pubkey()));
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
}
#[tokio::test] #[tokio::test]
async fn test_f32_multiply() { async fn test_f32_multiply() {
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));

View File

@ -1,6 +1,6 @@
[package] [package]
name = "spl-memo" name = "spl-memo"
version = "3.0.0" version = "3.0.1"
description = "Solana Program Library Memo" description = "Solana Program Library Memo"
authors = ["Solana Maintainers <maintainers@solana.foundation>"] authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library" repository = "https://github.com/solana-labs/solana-program-library"
@ -12,11 +12,11 @@ no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
solana-program = "1.6.2" solana-program = "1.6.7"
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -117,6 +117,7 @@ async fn test_memo_signing() {
} }
#[tokio::test] #[tokio::test]
#[ignore]
async fn test_memo_compute_limits() { async fn test_memo_compute_limits() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;

21
name-service/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
src/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
target
.vscode
hfuzz*
third_party/
*/node_modules
js/dist
js/lib
js/docs
js/src/secret.ts

10
name-service/README.md Normal file
View File

@ -0,0 +1,10 @@
# Name Service program
A spl program for issuing and managing ownership of: domain names, Solana Pubkeys, URLs, twitter handles, ipfs cid's, metadata, etc..
This program provides an interface and implementation that third parties can
utilize to create and use their own version of a name service of any kind.
Full documentation is available at https://spl.solana.com/name-service
JavaScript binding are available in the `./js` directory.

View File

@ -0,0 +1,52 @@
{
"name": "spl-name-service",
"version": "0.1.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/solana-labs/solana-program-library"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "tsc && node --trace-warnings dist/test.js",
"build": "tsc",
"prepublish": "tsc",
"lint": "yarn pretty && eslint .",
"lint:fix": "yarn pretty:fix && eslint . --fix",
"pretty": "prettier --check 'src/*.[jt]s'",
"pretty:fix": "prettier --write 'src/*.[jt]s'",
"doc": "yarn typedoc src/index.ts"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/bs58": "^4.0.1",
"@types/node": "^14.14.20",
"babel-eslint": "^10.1.0",
"eslint": "^7.17.0",
"eslint-plugin-import": "^2.22.1",
"nodemon": "^2.0.7",
"prettier": "^2.2.1",
"save-dev": "0.0.1-security",
"ts-node": "^9.1.1",
"tslib": "^2.2.0",
"typedoc": "^0.20.35",
"typescript": "^4.1.3"
},
"dependencies": {
"@project-serum/sol-wallet-adapter": "^0.1.5",
"@solana/spl-token": "0.1.3",
"@solana/web3.js": "^1.2.6",
"bip32": "^2.0.6",
"bn.js": "^5.1.3",
"@bonfida/borsh-js": "^0.3.1",
"bs58": "4.0.1",
"buffer-layout": "^1.2.0",
"core-util-is": "^1.0.2",
"crypto": "^1.0.1",
"crypto-ts": "^1.0.2",
"fs": "^0.0.1-security",
"tweetnacl": "^1.0.3",
"webpack-dev-server": "^3.11.2"
}
}

View File

@ -0,0 +1,190 @@
import {
Account,
Connection,
PublicKey,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import { Numberu64 } from "./utils";
import { updateInstruction, transferInstruction, createInstruction, deleteInstruction } from "./instructions";
import { createHash, HashOptions } from 'crypto';
import { getHashedName, getNameAccountKey, getNameOwner, Numberu32 } from ".";
import { hash } from "tweetnacl";
import { NameRegistryState } from "./state";
////////////////////////////////////////////////////////////
export const NAME_PROGRAM_ID = new PublicKey(
"Gh9eN9nDuS3ysmAkKf4QJ6yBzf3YNqsn6MD8Ms3TsXmA"
);
export const HASH_PREFIX = "SPL Name Service";
////////////////////////////////////////////////////////////
/**
* Creates a name account with the given rent budget, allocated space, owner and class.
*
* @param connection The solana connection object to the RPC node
* @param name The name of the new account
* @param space The space in bytes allocated to the account
* @param payerKey The allocation cost payer
* @param nameOwner The pubkey to be set as owner of the new name account
* @param lamports The budget to be set for the name account. If not specified, it'll be the minimum for rent exemption
* @param nameClass The class of this new name
* @param parentName The parent name of the new name. If specified its owner needs to sign
* @returns
*/
export async function createNameRegistry(
connection: Connection,
name: string,
space: number,
payerKey: PublicKey,
nameOwner: PublicKey,
lamports?: number,
nameClass?: PublicKey,
parentName?: PublicKey,
): Promise<TransactionInstruction> {
let hashed_name = await getHashedName(name);
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, parentName);
let balance = lamports
? lamports
: await connection.getMinimumBalanceForRentExemption(space);
let nameParentOwner: PublicKey | undefined;
if (!!parentName) {
let parentAccount = await getNameOwner(connection, parentName);
}
let createNameInstr = createInstruction(
NAME_PROGRAM_ID,
SystemProgram.programId,
nameAccountKey,
nameOwner,
payerKey,
hashed_name,
new Numberu64(balance),
new Numberu32(space),
nameClass,
parentName,
nameParentOwner
);
return createNameInstr;
}
/**
* Overwrite the data of the given name registry.
*
* @param connection The solana connection object to the RPC node
* @param name The name of the name registry to update
* @param offset The offset to which the data should be written into the registry
* @param input_data The data to be written
* @param nameClass The class of this name, if it exsists
* @param nameParent The parent name of this name, if it exists
*/
export async function updateNameRegistryData(
connection: Connection,
name: string,
offset: number,
input_data: Buffer,
nameClass?: PublicKey,
nameParent?: PublicKey,
): Promise<TransactionInstruction> {
let hashed_name = await getHashedName(name);
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
let signer: PublicKey;
if (!!nameClass) {
signer = nameClass;
} else {
signer = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
}
let updateInstr = updateInstruction(
NAME_PROGRAM_ID,
nameAccountKey,
new Numberu32(offset),
input_data,
signer
);
return updateInstr;
}
/**
* Cahnge the owner of a given name account.
*
* @param connection The solana connection object to the RPC node
* @param name The name of the name account
* @param newOwner The new owner to be set
* @param curentNameOwner the current name Owner
* @param nameClass The class of this name, if it exsists
* @param nameParent The parent name of this name, if it exists
* @returns
*/
export async function transferNameOwnership(
connection: Connection,
name: string,
newOwner: PublicKey,
currentNameOwner: PublicKey,
nameClass?: PublicKey,
nameParent?: PublicKey,
): Promise<TransactionInstruction> {
let hashed_name = await getHashedName(name);
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
let curentNameOwner: PublicKey;
if (!!nameClass) {
curentNameOwner = nameClass;
} else {
curentNameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
}
let transferInstr = transferInstruction(
NAME_PROGRAM_ID,
nameAccountKey,
newOwner,
curentNameOwner,
nameClass
);
return transferInstr;
}
/**
* Delete the name account and transfer the rent to the target.
*
* @param connection The solana connection object to the RPC node
* @param name The name of the name account
* @param refundTargetKey The refund destination address
* @param nameClass The class of this name, if it exsists
* @param nameParent The parent name of this name, if it exists
* @returns
*/
export async function deleteNameRegistry(
connection: Connection,
name: string,
refundTargetKey: PublicKey,
nameClass?: PublicKey,
nameParent?: PublicKey,
): Promise<TransactionInstruction> {
let hashed_name = await getHashedName(name);
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
let nameOwner: PublicKey;
if (!!nameClass) {
nameOwner = nameClass;
} else {
nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
}
let changeAuthoritiesInstr = deleteInstruction(
NAME_PROGRAM_ID,
nameAccountKey,
refundTargetKey,
nameOwner
);
return changeAuthoritiesInstr;
}

View File

@ -0,0 +1,2 @@
export * from "./utils";
export * from "./bindings";

View File

@ -0,0 +1,204 @@
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import { Schema, serialize } from "@bonfida/borsh-js";
import { Numberu64, Numberu32 } from "./utils";
export function createInstruction(
nameProgramId: PublicKey,
systemProgramId: PublicKey,
nameKey: PublicKey,
nameOwnerKey: PublicKey,
payerKey: PublicKey,
hashed_name: Buffer,
lamports: Numberu64,
space: Numberu32,
nameClassKey?: PublicKey,
nameParent?: PublicKey,
nameParentOwner?: PublicKey
): TransactionInstruction {
let buffers = [
Buffer.from(Int8Array.from([0])),
new Numberu32(hashed_name.length).toBuffer(),
hashed_name,
lamports.toBuffer(),
space.toBuffer(),
];
const data = Buffer.concat(buffers);
let keys = [
{
pubkey: systemProgramId,
isSigner: false,
isWritable: false,
},
{
pubkey: payerKey,
isSigner: true,
isWritable: true,
},
{
pubkey: nameKey,
isSigner: false,
isWritable: true,
},
{
pubkey: nameOwnerKey,
isSigner: false,
isWritable: false,
},
];
if (!!nameClassKey) {
keys.push({
pubkey: nameClassKey,
isSigner: true,
isWritable: false,
});
} else {
keys.push({
pubkey: new PublicKey(Buffer.alloc(32)),
isSigner: false,
isWritable: false,
});
}
if (!!nameParent) {
keys.push({
pubkey: nameParent,
isSigner: false,
isWritable: false,
});
} else {
keys.push({
pubkey: new PublicKey(Buffer.alloc(32)),
isSigner: false,
isWritable: false,
});
}
if (!!nameParentOwner) {
keys.push({
pubkey: nameParentOwner,
isSigner: true,
isWritable: false,
});
}
return new TransactionInstruction({
keys,
programId: nameProgramId,
data,
});
}
export function updateInstruction(
nameProgramId: PublicKey,
nameAccountKey: PublicKey,
offset: Numberu32,
input_data: Buffer,
nameUpdateSigner: PublicKey,
): TransactionInstruction {
let buffers = [
Buffer.from(Int8Array.from([1])),
offset.toBuffer(),
new Numberu32(input_data.length).toBuffer(),
input_data
];
const data = Buffer.concat(buffers);
let keys = [
{
pubkey: nameAccountKey,
isSigner: false,
isWritable: true,
},
{
pubkey: nameUpdateSigner,
isSigner: true,
isWritable: false,
},
];
return new TransactionInstruction({
keys,
programId: nameProgramId,
data,
});
}
export function transferInstruction(
nameProgramId: PublicKey,
nameAccountKey: PublicKey,
newOwnerKey: PublicKey,
currentNameOwnerKey: PublicKey,
nameClassKey?: PublicKey,
): TransactionInstruction {
let buffers = [
Buffer.from(Int8Array.from([2])),
newOwnerKey.toBuffer()
];
const data = Buffer.concat(buffers);
let keys = [
{
pubkey: nameAccountKey,
isSigner: false,
isWritable: true,
},
{
pubkey: currentNameOwnerKey,
isSigner: true,
isWritable: false,
},
];
if (!!nameClassKey) {
keys.push({
pubkey: nameClassKey,
isSigner: true,
isWritable: false,
});
}
return new TransactionInstruction({
keys,
programId: nameProgramId,
data,
});
}
export function deleteInstruction(
nameProgramId: PublicKey,
nameAccountKey: PublicKey,
refundTargetKey: PublicKey,
nameOwnerKey: PublicKey,
): TransactionInstruction {
let buffers = [
Buffer.from(Int8Array.from([3])),
];
const data = Buffer.concat(buffers);
let keys = [
{
pubkey: nameAccountKey,
isSigner: false,
isWritable: true,
},
{
pubkey: nameOwnerKey,
isSigner: true,
isWritable: false,
},
{
pubkey: refundTargetKey,
isSigner: false,
isWritable: true,
},
];
return new TransactionInstruction({
keys,
programId: nameProgramId,
data,
});
}

View File

@ -0,0 +1,56 @@
import { PublicKey, Connection } from "@solana/web3.js";
import { Schema, deserializeUnchecked } from "@bonfida/borsh-js";
export class NameRegistryState {
parentName: PublicKey;
owner: PublicKey;
class: PublicKey;
data: Buffer;
static schema: Schema = new Map([
[
NameRegistryState,
{
kind: 'struct',
fields: [
['parentName', [32]],
['owner', [32]],
['class', [32]],
['data', ['u8']],
],
},
],
]);
constructor(obj: {
parentName: Uint8Array;
owner: Uint8Array;
class: Uint8Array;
data: Uint8Array;
}) {
this.parentName = new PublicKey(obj.parentName);
this.owner = new PublicKey(obj.owner);
this.class = new PublicKey(obj.class);
this.data = Buffer.from(obj.data);
}
static async retrieve(
connection: Connection,
nameAccountKey: PublicKey,
): Promise<NameRegistryState> {
let nameAccount = await connection.getAccountInfo(
nameAccountKey,
'processed',
);
if (!nameAccount) {
throw new Error('Invalid name account provided');
}
let res: NameRegistryState = deserializeUnchecked(
this.schema,
NameRegistryState,
nameAccount.data,
);
return res;
}
}

View File

@ -0,0 +1,93 @@
import { serialize } from "@bonfida/borsh-js";
import { Connection, Account, PublicKey, AccountInfo } from "@solana/web3.js";
import { transferNameOwnership, updateNameRegistryData, createNameRegistry, deleteNameRegistry } from "./bindings";
import { readFile } from "fs/promises";
import { Numberu64, signAndSendTransactionInstructions } from "./utils";
import { sign } from "tweetnacl";
import { getHashedName, getNameAccountKey, Numberu32 } from ".";
import { NameRegistryState } from "./state";
const ENDPOINT = 'https://devnet.solana.com/';
// const ENDPOINT = 'https://solana-api.projectserum.com/';
export async function test() {
let connection = new Connection(ENDPOINT);
let secretKey = JSON.parse(
(await readFile('/home/lcchy-work/.config/solana/id_devnet.json')).toString()
);
let adminAccount = new Account(secretKey);
let root_name = ".sol";
// let create_instruction = await createNameRegistry(
// connection,
// root_name,
// 1000,
// adminAccount.publicKey,
// adminAccount.publicKey,
// );
// console.log(
// await signAndSendTransactionInstructions(
// connection,
// [adminAccount],
// adminAccount,
// [create_instruction]
// )
// );
// let input_data = Buffer.from("Du");
// let updateInstruction = await updateNameRegistryData(
// connection,
// root_name,
// 0,
// input_data,
// );
// console.log(
// await signAndSendTransactionInstructions(
// connection,
// [adminAccount],
// adminAccount,
// [updateInstruction]
// )
// );
// let transferInstruction = await transferNameOwnership(
// connection,
// root_name,
// adminAccount.publicKey,
// adminAccount.publicKey,
// );
// console.log(
// await signAndSendTransactionInstructions(
// connection,
// [adminAccount],
// adminAccount,
// [transferInstruction]
// )
// );
// let deleteInstruction = await deleteNameRegistry(
// connection,
// root_name,
// adminAccount.publicKey
// );
// console.log(
// await signAndSendTransactionInstructions(
// connection,
// [adminAccount],
// adminAccount,
// [deleteInstruction]
// )
// );
let hashed_root_name = await getHashedName(root_name);
let nameAccountKey = await getNameAccountKey(hashed_root_name);
console.log(await NameRegistryState.retrieve(connection, nameAccountKey));
}
test();

View File

@ -0,0 +1,132 @@
import {
PublicKey,
TransactionInstruction,
Connection,
Account,
Transaction,
AccountInfo,
} from "@solana/web3.js";
import assert from "assert";
import BN from "bn.js";
import { createHash } from "crypto";
import { HASH_PREFIX, NAME_PROGRAM_ID } from ".";
import { NameRegistryState} from "./state";
export class Numberu32 extends BN {
/**
* Convert to Buffer representation
*/
toBuffer(): Buffer {
const a = super.toArray().reverse();
const b = Buffer.from(a);
if (b.length === 4) {
return b;
}
assert(b.length < 4, "Numberu32 too large");
const zeroPad = Buffer.alloc(4);
b.copy(zeroPad);
return zeroPad;
}
/**
* Construct a Numberu64 from Buffer representation
*/
static fromBuffer(buffer): any {
assert(buffer.length === 4, `Invalid buffer length: ${buffer.length}`);
return new BN(
[...buffer]
.reverse()
.map((i) => `00${i.toString(16)}`.slice(-2))
.join(""),
16
);
}
}
export class Numberu64 extends BN {
/**
* Convert to Buffer representation
*/
toBuffer(): Buffer {
const a = super.toArray().reverse();
const b = Buffer.from(a);
if (b.length === 8) {
return b;
}
assert(b.length < 8, "Numberu64 too large");
const zeroPad = Buffer.alloc(8);
b.copy(zeroPad);
return zeroPad;
}
/**
* Construct a Numberu64 from Buffer representation
*/
static fromBuffer(buffer): any {
assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`);
return new BN(
[...buffer]
.reverse()
.map((i) => `00${i.toString(16)}`.slice(-2))
.join(""),
16
);
}
}
export const signAndSendTransactionInstructions = async (
// sign and send transaction
connection: Connection,
signers: Array<Account>,
feePayer: Account,
txInstructions: Array<TransactionInstruction>
): Promise<string> => {
const tx = new Transaction();
tx.feePayer = feePayer.publicKey;
signers.push(feePayer);
tx.add(...txInstructions);
return await connection.sendTransaction(tx, signers, {
preflightCommitment: "single",
});
};
export async function getHashedName(
name: string
): Promise<Buffer> {
let input = HASH_PREFIX + name;
let buffer = createHash('sha256').update(input, 'utf8').digest();
return buffer
}
export async function getNameAccountKey(
hashed_name: Buffer,
nameClass?: PublicKey,
nameParent?: PublicKey
): Promise<PublicKey> {
let seeds = [hashed_name];
if (!!nameClass) {
seeds.push(nameClass.toBuffer());
} else {
seeds.push(Buffer.alloc(32));
}
if (!!nameParent) {
seeds.push(nameParent.toBuffer());
} else {
seeds.push(Buffer.alloc(32));
}
let [nameAccountKey, _] = await PublicKey.findProgramAddress(
seeds,
NAME_PROGRAM_ID
);
return nameAccountKey
}
export async function getNameOwner(connection: Connection, nameAccountKey: PublicKey): Promise<NameRegistryState> {
let nameAccount = await connection.getAccountInfo(nameAccountKey);
if (!nameAccount) {
throw "Unable to find the given account."
}
return NameRegistryState.retrieve(connection, nameAccountKey); //TODO use borsh-js
}

View File

@ -0,0 +1,32 @@
{
"extends": "@tsconfig/recommended/tsconfig.json",
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*" : ["types/*"]
}
}
},
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es2019",
"outDir": "dist",
"rootDir": "./src",
"declaration": true,
"noImplicitAny": false,
"moduleResolution": "node",
"sourceMap": true,
"baseUrl": ".",
"paths": {
"*": ["node_modules/*", "src/types/*"]
},
"resolveJsonModule": true
},
"include": ["src/*"],
"exclude": ["src/**/*.test.ts", "**/node_modules", "dist"]
}

4819
name-service/js/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
[package]
name = "spl-name-service"
description = "Solana Program Library Name Service"
version = "0.1.0"
repository = "https://github.com/solana-labs/solana-program-library"
authors = ["lcchy <lucas@bonfida.com>"]
license = "Apache-2.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
solana-program = "1.6.7"
num-traits = "0.2"
borsh = "0.8.1"
num-derive = "0.3.3"
thiserror = "1.0.24"
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1 @@
namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX

View File

@ -0,0 +1,36 @@
use {
crate::error::NameServiceError,
crate::processor::Processor,
num_traits::FromPrimitive,
solana_program::{
account_info::AccountInfo, decode_error::DecodeError, entrypoint,
entrypoint::ProgramResult, msg, program_error::PrintProgramError, pubkey::Pubkey,
},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Entrypoint");
if let Err(error) = Processor::process_instruction(program_id, accounts, instruction_data) {
// catch the error so we can print it
error.print::<NameServiceError>();
return Err(error);
}
Ok(())
}
impl PrintProgramError for NameServiceError {
fn print<E>(&self)
where
E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive,
{
match self {
NameServiceError::OutOfSpace => msg!("Error: Registry is out of space!"),
}
}
}

View File

@ -0,0 +1,25 @@
use {
num_derive::FromPrimitive,
solana_program::{decode_error::DecodeError, program_error::ProgramError},
thiserror::Error,
};
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum NameServiceError {
#[error("Out of space")]
OutOfSpace,
}
pub type NameServiceResult = Result<(), NameServiceError>;
impl From<NameServiceError> for ProgramError {
fn from(e: NameServiceError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for NameServiceError {
fn type_of() -> &'static str {
"NameServiceError"
}
}

View File

@ -0,0 +1,189 @@
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
system_program,
},
};
/// Instructions supported by the generic Name Registry program
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)]
pub enum NameRegistryInstruction {
/// Create an empty name record
///
/// The address of the name record (account #1) is a program-derived address with the following
/// seeds to ensure uniqueness:
/// * SHA256(HASH_PREFIX, `Create::name`)
/// * Account class (account #3)
/// * Parent name record address (account #4)
///
/// If this is a child record, the parent record's owner must approve by signing (account #5)
///
/// Accounts expected by this instruction:
/// 0. `[]` System program
/// 1. `[writeable, signer]` Funding account (must be a system account)
/// 2. `[writeable]` Name record to be created (program-derived address)
/// 3. `[]` Account owner (written into `NameRecordHeader::owner`)
/// 4. `[signer]` Account class (written into `NameRecordHeader::class`).
/// If `Pubkey::default()` then the `signer` bit is not required
/// 5. `[]` Parent name record (written into `NameRecordHeader::parent_name). `Pubkey::default()` is equivalent to no existing parent.
/// 6. `[signer]` Owner of the parent name record. Optional but needed if parent name different than default.
///
Create {
/// SHA256 of the (HASH_PREFIX + Name) of the record to create, hashing is done off-chain
hashed_name: Vec<u8>,
/// Number of lamports to fund the name record with
lamports: u64,
/// Number of bytes of memory to allocate in addition to the `NameRecordHeader`
space: u32,
},
/// Update the data in a name record
///
/// Accounts expected by this instruction:
/// * If account class is `Pubkey::default()`:
/// 0. `[writeable]` Name record to be updated
/// 1. `[signer]` Account owner
///
/// * If account class is not `Pubkey::default()`:
/// 0. `[writeable]` Name record to be updated
/// 1. `[signer]` Account class
///
Update { offset: u32, data: Vec<u8> },
/// Transfer ownership of a name record
///
/// Accounts expected by this instruction:
///
/// * If account class is `Pubkey::default()`:
/// 0. `[writeable]` Name record to be transferred
/// 1. `[signer]` Account owner
///
/// * If account class is not `Pubkey::default()`:
/// 0. `[writeable]` Name record to be transferred
/// 1. `[signer]` Account owner
/// 1. `[signer]` Account class
///
Transfer { new_owner: Pubkey },
/// Delete a name record.
///
/// Any lamports remaining in the name record will be transferred to the refund account (#2)
///
/// Accounts expected by this instruction:
/// 0. `[writeable]` Name record to be deleted
/// 1. `[signer]` Account owner
/// 2. `[writeable]` Refund account
///
Delete,
}
#[allow(clippy::clippy::too_many_arguments)]
pub fn create(
name_service_program_id: Pubkey,
instruction_data: NameRegistryInstruction,
name_account_key: Pubkey,
payer_key: Pubkey,
name_owner: Pubkey,
name_class_opt: Option<Pubkey>,
name_parent_opt: Option<Pubkey>,
name_parent_owner_opt: Option<Pubkey>,
) -> Result<Instruction, ProgramError> {
let data = instruction_data.try_to_vec().unwrap();
let mut accounts = vec![
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new(payer_key, true),
AccountMeta::new(name_account_key, false),
AccountMeta::new_readonly(name_owner, false),
];
if let Some(name_class) = name_class_opt {
accounts.push(AccountMeta::new_readonly(name_class, true));
} else {
accounts.push(AccountMeta::new_readonly(Pubkey::default(), false));
}
if let Some(name_parent) = name_parent_opt {
accounts.push(AccountMeta::new_readonly(name_parent, false));
} else {
accounts.push(AccountMeta::new_readonly(Pubkey::default(), false));
}
if let Some(key) = name_parent_owner_opt {
accounts.push(AccountMeta::new_readonly(key, true));
}
Ok(Instruction {
program_id: name_service_program_id,
accounts,
data,
})
}
pub fn update(
name_service_program_id: Pubkey,
offset: u32,
data: Vec<u8>,
name_account_key: Pubkey,
name_update_signer: Pubkey,
) -> Result<Instruction, ProgramError> {
let instruction_data = NameRegistryInstruction::Update { offset, data };
let data = instruction_data.try_to_vec().unwrap();
let accounts = vec![
AccountMeta::new(name_account_key, false),
AccountMeta::new_readonly(name_update_signer, true),
];
Ok(Instruction {
program_id: name_service_program_id,
accounts,
data,
})
}
pub fn transfer(
name_service_program_id: Pubkey,
new_owner: Pubkey,
name_account_key: Pubkey,
name_owner_key: Pubkey,
name_class_opt: Option<Pubkey>,
) -> Result<Instruction, ProgramError> {
let instruction_data = NameRegistryInstruction::Transfer { new_owner };
let data = instruction_data.try_to_vec().unwrap();
let mut accounts = vec![
AccountMeta::new(name_account_key, false),
AccountMeta::new_readonly(name_owner_key, true),
];
if let Some(key) = name_class_opt {
accounts.push(AccountMeta::new_readonly(key, true));
}
Ok(Instruction {
program_id: name_service_program_id,
accounts,
data,
})
}
pub fn delete(
name_service_program_id: Pubkey,
name_account_key: Pubkey,
name_owner_key: Pubkey,
refund_target: Pubkey,
) -> Result<Instruction, ProgramError> {
let instruction_data = NameRegistryInstruction::Delete;
let data = instruction_data.try_to_vec().unwrap();
let accounts = vec![
AccountMeta::new(name_account_key, false),
AccountMeta::new_readonly(name_owner_key, true),
AccountMeta::new(refund_target, false),
];
Ok(Instruction {
program_id: name_service_program_id,
accounts,
data,
})
}

View File

@ -0,0 +1,11 @@
#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint;
pub mod error;
pub mod instruction;
pub mod processor;
pub mod state;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;
solana_program::declare_id!("namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX");

View File

@ -0,0 +1,242 @@
use {
crate::{
instruction::NameRegistryInstruction,
state::get_seeds_and_key,
state::{write_data, NameRecordHeader},
},
borsh::BorshDeserialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
system_instruction,
},
};
pub struct Processor {}
impl Processor {
pub fn process_create(
program_id: &Pubkey,
accounts: &[AccountInfo],
hashed_name: Vec<u8>,
lamports: u64,
space: u32,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let system_program = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let name_account = next_account_info(accounts_iter)?;
let name_owner = next_account_info(accounts_iter)?;
let name_class = next_account_info(accounts_iter)?;
let parent_name_account = next_account_info(accounts_iter)?;
let parent_name_owner = next_account_info(accounts_iter).ok();
let (name_account_key, seeds) = get_seeds_and_key(
&program_id,
hashed_name,
Some(name_class.key),
Some(parent_name_account.key),
);
// Verifications
if name_account_key != *name_account.key {
msg!("The given name account is incorrect.");
return Err(ProgramError::InvalidArgument);
}
if name_account.data.borrow().len() > 0 {
let name_record_header =
NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;
if name_record_header.owner != Pubkey::default() {
msg!("The given name account already exists.");
return Err(ProgramError::InvalidArgument);
}
}
if *name_class.key != Pubkey::default() && !name_class.is_signer {
msg!("The given name class is not a signer.");
return Err(ProgramError::InvalidArgument);
}
if *parent_name_account.key != Pubkey::default() {
if !parent_name_owner.unwrap().is_signer {
msg!("The given parent name account owner is not a signer.");
return Err(ProgramError::InvalidArgument);
} else {
let parent_name_record_header =
NameRecordHeader::unpack_from_slice(&parent_name_account.data.borrow())?;
if &parent_name_record_header.owner != parent_name_owner.unwrap().key {
msg!("The given parent name account owner is not correct.");
return Err(ProgramError::InvalidArgument);
}
}
}
if name_owner.key == &Pubkey::default() {
msg!("The owner cannot be `Pubkey::default()`.");
return Err(ProgramError::InvalidArgument);
}
if name_account.data.borrow().len() == 0 {
// Issue the name registry account
// The creation is done in three steps: transfer, allocate and assign, because
// one cannot `system_instruction::create` an account to which lamports have been transfered before.
invoke(
&system_instruction::transfer(&payer_account.key, &name_account_key, lamports),
&[
payer_account.clone(),
name_account.clone(),
system_program.clone(),
],
)?;
invoke_signed(
&system_instruction::allocate(&name_account_key, space as u64),
&[name_account.clone(), system_program.clone()],
&[&seeds.chunks(32).collect::<Vec<&[u8]>>()],
)?;
invoke_signed(
&system_instruction::assign(name_account.key, &program_id),
&[name_account.clone(), system_program.clone()],
&[&seeds.chunks(32).collect::<Vec<&[u8]>>()],
)?;
}
let name_state = NameRecordHeader {
parent_name: *parent_name_account.key,
owner: *name_owner.key,
class: *name_class.key,
};
name_state.pack_into_slice(&mut name_account.data.borrow_mut());
Ok(())
}
pub fn process_update(accounts: &[AccountInfo], offset: u32, data: Vec<u8>) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let name_account = next_account_info(accounts_iter)?;
let name_update_signer = next_account_info(accounts_iter)?;
let name_record_header = NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;
// Verifications
if !name_update_signer.is_signer {
msg!("The given name class or owner is not a signer.");
return Err(ProgramError::InvalidArgument);
}
if name_record_header.class != Pubkey::default()
&& *name_update_signer.key != name_record_header.class
{
msg!("The given name class account is incorrect.");
return Err(ProgramError::InvalidArgument);
}
if name_record_header.class == Pubkey::default()
&& *name_update_signer.key != name_record_header.owner
{
msg!("The given name owner account is incorrect.");
return Err(ProgramError::InvalidArgument);
}
write_data(name_account, &data, NameRecordHeader::LEN + offset as usize);
Ok(())
}
pub fn process_transfer(accounts: &[AccountInfo], new_owner: Pubkey) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let name_account = next_account_info(accounts_iter)?;
let name_owner = next_account_info(accounts_iter)?;
let name_class_opt = next_account_info(accounts_iter).ok();
let mut name_record_header =
NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;
// Verifications
if !name_owner.is_signer || name_record_header.owner != *name_owner.key {
msg!("The given name owner is incorrect or not a signer.");
return Err(ProgramError::InvalidArgument);
}
if name_record_header.class != Pubkey::default()
&& (name_class_opt.is_none()
|| name_record_header.class != *name_class_opt.unwrap().key
|| !name_class_opt.unwrap().is_signer)
{
msg!("The given name class account is incorrect or not a signer.");
return Err(ProgramError::InvalidArgument);
}
name_record_header.owner = new_owner;
name_record_header
.pack_into_slice(&mut name_account.data.borrow_mut()[..NameRecordHeader::LEN]);
Ok(())
}
pub fn process_delete(accounts: &[AccountInfo]) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let name_account = next_account_info(accounts_iter)?;
let name_owner = next_account_info(accounts_iter)?;
let refund_target = next_account_info(accounts_iter)?;
let name_record_header = NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;
// Verifications
if !name_owner.is_signer || name_record_header.owner != *name_owner.key {
msg!("The given name owner is incorrect or not a signer.");
return Err(ProgramError::InvalidArgument);
}
// Overwrite the data with zeroes
write_data(name_account, &vec![0; name_account.data_len()], 0);
// Close the account by transferring the rent sol
let source_amount: &mut u64 = &mut name_account.lamports.borrow_mut();
let dest_amount: &mut u64 = &mut refund_target.lamports.borrow_mut();
*dest_amount += *source_amount;
*source_amount = 0;
Ok(())
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Beginning processing");
let instruction = NameRegistryInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
msg!("Instruction unpack_from_sliceed");
match instruction {
NameRegistryInstruction::Create {
hashed_name,
lamports,
space,
} => {
msg!("Instruction: Create");
Processor::process_create(program_id, accounts, hashed_name, lamports, space)?;
}
NameRegistryInstruction::Update { offset, data } => {
msg!("Instruction: Update Data");
Processor::process_update(accounts, offset, data)?;
}
NameRegistryInstruction::Transfer { new_owner } => {
msg!("Instruction: Transfer Ownership");
Processor::process_transfer(accounts, new_owner)?;
}
NameRegistryInstruction::Delete => {
msg!("Instruction: Delete Name");
Processor::process_delete(accounts)?;
}
}
Ok(())
}
}

View File

@ -0,0 +1,92 @@
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::AccountInfo,
msg,
program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed},
pubkey::Pubkey,
},
};
/// The data for a Name Registry account is always prefixed a `NameRecordHeader` structure.
///
/// The layout of the remaining bytes in the account data are determined by the record `class`
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct NameRecordHeader {
// Names are hierarchical. `parent_name` contains the account address of the parent
// name, or `Pubkey::default()` if no parent exists.
pub parent_name: Pubkey,
// The owner of this name
pub owner: Pubkey,
// The class of data this account represents (DNS record, twitter handle, SPL Token name/symbol, etc)
//
// If `Pubkey::default()` the data is unspecified.
pub class: Pubkey,
}
impl Sealed for NameRecordHeader {}
impl Pack for NameRecordHeader {
const LEN: usize = 96;
fn pack_into_slice(&self, dst: &mut [u8]) {
let mut slice = dst;
self.serialize(&mut slice).unwrap()
}
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let mut p = src;
NameRecordHeader::deserialize(&mut p).map_err(|_| {
msg!("Failed to deserialize name record");
ProgramError::InvalidAccountData
})
}
}
impl IsInitialized for NameRecordHeader {
fn is_initialized(&self) -> bool {
self.owner == Pubkey::default()
}
}
pub fn write_data(account: &AccountInfo, input: &[u8], offset: usize) {
let mut account_data = account.data.borrow_mut();
account_data[offset..offset + input.len()].copy_from_slice(input);
}
////////////////////////////////////////////////////////////
pub const HASH_PREFIX: &str = "SPL Name Service";
////////////////////////////////////////////////////////////
pub fn get_seeds_and_key(
program_id: &Pubkey,
hashed_name: Vec<u8>, // Hashing is done off-chain
name_class_opt: Option<&Pubkey>,
parent_name_address_opt: Option<&Pubkey>,
) -> (Pubkey, Vec<u8>) {
// let hashed_name: Vec<u8> = hashv(&[(HASH_PREFIX.to_owned() + name).as_bytes()]).0.to_vec();
let mut seeds_vec: Vec<u8> = hashed_name;
let name_class = name_class_opt.cloned().unwrap_or_default();
for b in name_class.to_bytes().to_vec() {
seeds_vec.push(b);
}
let parent_name_address = parent_name_address_opt.cloned().unwrap_or_default();
for b in parent_name_address.to_bytes().to_vec() {
seeds_vec.push(b);
}
let (name_account_key, bump) =
Pubkey::find_program_address(&seeds_vec.chunks(32).collect::<Vec<&[u8]>>(), program_id);
seeds_vec.push(bump);
(name_account_key, seeds_vec)
}

View File

@ -0,0 +1,204 @@
#![cfg(feature = "test-bpf")]
use std::str::FromStr;
use solana_program::{instruction::Instruction, program_pack::Pack, pubkey::Pubkey};
use solana_program_test::{processor, tokio, ProgramTest, ProgramTestContext};
use solana_program::hash::hashv;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
transport::TransportError,
};
use spl_name_service::{
entrypoint::process_instruction,
instruction::{create, delete, transfer, update, NameRegistryInstruction},
state::{get_seeds_and_key, NameRecordHeader, HASH_PREFIX},
};
#[tokio::test]
async fn test_name_service() {
// Create program and test environment
let program_id = Pubkey::from_str("XCWuBvfNamesXCWuBvfkegQfZyiNwAJb9Ss623VQ5DA").unwrap();
let program_test = ProgramTest::new(
"spl_name_service",
program_id,
processor!(process_instruction),
);
let mut ctx = program_test.start_with_context().await;
let root_name = ".sol";
let tld_class = Keypair::new();
let owner = Keypair::new();
let hashed_root_name: Vec<u8> = hashv(&[(HASH_PREFIX.to_owned() + root_name).as_bytes()])
.0
.to_vec();
let (root_name_account_key, _) = get_seeds_and_key(
&program_id,
hashed_root_name.clone(),
Some(&tld_class.pubkey()),
None,
);
let create_name_instruction = create(
program_id,
NameRegistryInstruction::Create {
hashed_name: hashed_root_name,
lamports: 1_000_000,
space: 1_000,
},
root_name_account_key,
ctx.payer.pubkey(),
owner.pubkey(),
Some(tld_class.pubkey()),
None,
None,
)
.unwrap();
sign_send_instruction(&mut ctx, create_name_instruction, vec![&tld_class])
.await
.unwrap();
let name_record_header = NameRecordHeader::unpack_from_slice(
&mut &ctx
.banks_client
.get_account(root_name_account_key)
.await
.unwrap()
.unwrap()
.data,
)
.unwrap();
println!("Name Record Header: {:?}", name_record_header);
let name = "bonfida";
let sol_subdomains_class = Keypair::new();
let hashed_name: Vec<u8> = hashv(&[(HASH_PREFIX.to_owned() + name).as_bytes()])
.0
.to_vec();
let (name_account_key, _) = get_seeds_and_key(
&program_id,
hashed_name.clone(),
Some(&sol_subdomains_class.pubkey()),
Some(&root_name_account_key),
);
let create_name_instruction = create(
program_id,
NameRegistryInstruction::Create {
hashed_name,
lamports: 1_000_000,
space: 1_000,
},
name_account_key,
ctx.payer.pubkey(),
owner.pubkey(),
Some(sol_subdomains_class.pubkey()),
Some(root_name_account_key),
Some(owner.pubkey()),
)
.unwrap();
sign_send_instruction(
&mut ctx,
create_name_instruction,
vec![&sol_subdomains_class, &owner],
)
.await
.unwrap();
let name_record_header = NameRecordHeader::unpack_from_slice(
&mut &ctx
.banks_client
.get_account(name_account_key)
.await
.unwrap()
.unwrap()
.data,
)
.unwrap();
println!("Name Record Header: {:?}", name_record_header);
println!("SOl class {:?}", sol_subdomains_class.pubkey());
let data = "@Dudl".as_bytes().to_vec();
let update_instruction = update(
program_id,
0,
data,
name_account_key,
sol_subdomains_class.pubkey(),
)
.unwrap();
sign_send_instruction(&mut ctx, update_instruction, vec![&sol_subdomains_class])
.await
.unwrap();
let name_record_header = NameRecordHeader::unpack_from_slice(
&mut &ctx
.banks_client
.get_account(name_account_key)
.await
.unwrap()
.unwrap()
.data,
)
.unwrap();
println!("Name Record Header: {:?}", name_record_header);
let transfer_instruction = transfer(
program_id,
ctx.payer.pubkey(),
name_account_key,
owner.pubkey(),
Some(sol_subdomains_class.pubkey()),
)
.unwrap();
sign_send_instruction(
&mut ctx,
transfer_instruction,
vec![&owner, &sol_subdomains_class],
)
.await
.unwrap();
let name_record_header = NameRecordHeader::unpack_from_slice(
&mut &ctx
.banks_client
.get_account(name_account_key)
.await
.unwrap()
.unwrap()
.data,
)
.unwrap();
println!("Name Record Header: {:?}", name_record_header);
let delete_instruction = delete(
program_id,
name_account_key,
ctx.payer.pubkey(),
ctx.payer.pubkey(),
)
.unwrap();
sign_send_instruction(&mut ctx, delete_instruction, vec![])
.await
.unwrap();
}
// Utils
pub async fn sign_send_instruction(
ctx: &mut ProgramTestContext,
instruction: Instruction,
signers: Vec<&Keypair>,
) -> Result<(), TransportError> {
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&ctx.payer.pubkey()));
let mut payer_signers = vec![&ctx.payer];
for s in signers {
payer_signers.push(s);
}
transaction.partial_sign(&payer_signers, ctx.last_blockhash);
ctx.banks_client.process_transaction(transaction).await
}

View File

@ -16,12 +16,12 @@ borsh = "0.8.1"
borsh-derive = "0.8.1" borsh-derive = "0.8.1"
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
solana-program = "1.6.2" solana-program = "1.6.7"
thiserror = "1.0" thiserror = "1.0"
[dev-dependencies] [dev-dependencies]
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -7,14 +7,16 @@ repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0" license = "Apache-2.0"
edition = "2018" edition = "2018"
[features]
test-bpf = []
[dependencies] [dependencies]
arrayref = "0.3.6" arrayref = "0.3.6"
solana-program = "1.6.2" solana-program = "=1.6.7"
[dev-dependencies] [dev-dependencies]
solana-bpf-loader-program = "1.6.2" solana-program-test = "=1.6.7"
solana-sdk = "1.6.2" solana-sdk = "=1.6.7"
solana_rbpf = "0.2"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -1,180 +1,185 @@
use solana_bpf_loader_program::serialization::serialize_parameters; // Program test does not support calling a raw program entrypoint, only `process_instruction`
use solana_program::{ #![cfg(feature = "test-bpf")]
bpf_loader, entrypoint::SUCCESS, program_error::ProgramError, pubkey::Pubkey,
use solana_program_test::*;
use solana_sdk::{
account::Account,
instruction::InstructionError,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::Signer,
transaction::{Transaction, TransactionError},
}; };
use solana_sdk::{account::AccountSharedData, keyed_account::KeyedAccount};
use spl_shared_memory::entrypoint;
// TODO: Rework `assert_instruction_count` test to use solana-program-test, avoiding the need to #[tokio::test]
// link directly with the BPF VM async fn assert_instruction_count() {
/*
fn load_program(name: &str) -> Vec<u8> {
let mut file =
File::open(&name).unwrap_or_else(|err| panic!("Unable to open {}: {}", name, err));
let mut program = Vec::new();
file.read_to_end(&mut program).unwrap();
program
}
fn run_program(
program_id: &Pubkey,
parameter_accounts: &[KeyedAccount],
instruction_data: &[u8],
) -> u64 {
let program_account = Account {
data: load_program("../../target/deploy/spl_shared_memory.so"),
..Account::default()
};
let loader_id = bpf_loader::id();
let mut invoke_context = MockInvokeContext::default();
let executable = EbpfVm::<solana_bpf_loader_program::BPFError>::create_executable_from_elf(
&&program_account.data,
None,
)
.unwrap();
let (mut vm, heap_region) = create_vm(
&loader_id,
executable.as_ref(),
parameter_accounts,
&mut invoke_context,
)
.unwrap();
let mut parameter_bytes = serialize_parameters(
&loader_id,
program_id,
parameter_accounts,
&instruction_data,
)
.unwrap();
assert_eq!(
SUCCESS,
vm.execute_program(parameter_bytes.as_mut_slice(), &[], &[heap_region])
.unwrap()
);
deserialize_parameters(&loader_id, parameter_accounts, &parameter_bytes).unwrap();
vm.get_total_instruction_count()
}
#[test]
fn assert_instruction_count() {
const OFFSET: usize = 51; const OFFSET: usize = 51;
const NUM_TO_SHARE: usize = 500; const NUM_TO_SHARE: usize = 500;
let program_id = Pubkey::new_unique(); let program_id = Pubkey::new_unique();
let shared_key = Pubkey::new_unique(); let shared_key = Pubkey::new_unique();
let shared_account = Account::new_ref(u64::MAX, OFFSET + NUM_TO_SHARE * 2, &program_id);
// Send some data to share let mut program_test = ProgramTest::new(
let parameter_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)]; "spl_shared_memory", // Run the BPF version with `cargo test-bpf`
let content = vec![42; NUM_TO_SHARE]; program_id,
let mut instruction_data = OFFSET.to_le_bytes().to_vec(); None,
instruction_data.extend_from_slice(&content);
let share_count = run_program(&program_id, &parameter_accounts[..], &instruction_data);
const BASELINE_COUNT: u64 = 1474; // 113 if NUM_TO_SHARE is 8
println!(
"BPF instructions executed {:?} (expected {:?})",
share_count, BASELINE_COUNT
); );
assert_eq!( program_test.add_account(
&shared_account.borrow().data[OFFSET..OFFSET + NUM_TO_SHARE], shared_key,
content Account {
lamports: 5000000000000,
data: vec![0_u8; NUM_TO_SHARE * 2],
owner: program_id,
..Account::default()
},
); );
assert!(share_count <= BASELINE_COUNT); program_test.set_bpf_compute_max_units(480);
} let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
*/
#[test]
fn test_share_data() {
const OFFSET: usize = 51;
const NUM_TO_SHARE: usize = 500;
let program_id = Pubkey::new(&[0; 32]);
let shared_key = Pubkey::new_unique();
let shared_account = AccountSharedData::new_ref(u64::MAX, NUM_TO_SHARE * 2, &program_id);
// success // success
let content = vec![42; NUM_TO_SHARE]; let content = vec![42; NUM_TO_SHARE];
let mut instruction_data = OFFSET.to_le_bytes().to_vec(); let mut instruction_data = OFFSET.to_le_bytes().to_vec();
instruction_data.extend_from_slice(&content); instruction_data.extend_from_slice(&content);
let keyed_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)]; let mut transaction = Transaction::new_with_payer(
let mut input = serialize_parameters( &[Instruction::new_with_bytes(
&bpf_loader::id(), program_id,
&program_id, &instruction_data,
&keyed_accounts, vec![AccountMeta::new(shared_key, false)],
&instruction_data, )],
) Some(&payer.pubkey()),
.unwrap(); );
assert_eq!(unsafe { entrypoint(input.as_mut_ptr()) }, SUCCESS); transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
}
#[tokio::test]
async fn test_helloworld() {
const OFFSET: usize = 51;
const NUM_TO_SHARE: usize = 500;
let program_id = Pubkey::new_unique();
let shared_key = Pubkey::new_unique();
let mut program_test = ProgramTest::new(
"spl_shared_memory", // Run the BPF version with `cargo test-bpf`
program_id,
None,
);
program_test.add_account(
shared_key,
Account {
lamports: 5000000000000,
data: vec![0_u8; NUM_TO_SHARE * 2],
owner: program_id,
..Account::default()
},
);
let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
// success
let content = vec![42; NUM_TO_SHARE];
let mut instruction_data = OFFSET.to_le_bytes().to_vec();
instruction_data.extend_from_slice(&content);
let mut transaction = Transaction::new_with_payer(
&[Instruction::new_with_bytes(
program_id,
&instruction_data,
vec![AccountMeta::new(shared_key, false)],
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// success zero offset // success zero offset
let content = vec![42; NUM_TO_SHARE]; let content = vec![42; NUM_TO_SHARE];
let mut instruction_data = 0_usize.to_le_bytes().to_vec(); let mut instruction_data = 0_usize.to_le_bytes().to_vec();
instruction_data.extend_from_slice(&content); instruction_data.extend_from_slice(&content);
let keyed_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)]; let mut transaction = Transaction::new_with_payer(
let mut input = serialize_parameters( &[Instruction::new_with_bytes(
&bpf_loader::id(), program_id,
&program_id, &instruction_data,
&keyed_accounts, vec![AccountMeta::new(shared_key, false)],
&instruction_data, )],
) Some(&payer.pubkey()),
.unwrap(); );
assert_eq!(unsafe { entrypoint(input.as_mut_ptr()) }, SUCCESS); transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// too few accounts // too few accounts
let mut input = let content = vec![42; NUM_TO_SHARE];
serialize_parameters(&bpf_loader::id(), &program_id, &[], &instruction_data).unwrap(); let mut instruction_data = OFFSET.to_le_bytes().to_vec();
instruction_data.extend_from_slice(&content);
let mut transaction = Transaction::new_with_payer(
&[Instruction::new_with_bytes(
program_id,
&instruction_data,
vec![],
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
let result = banks_client.process_transaction(transaction).await;
assert_eq!( assert_eq!(
unsafe { entrypoint(input.as_mut_ptr()) }, result.unwrap_err().unwrap(),
u64::from(ProgramError::NotEnoughAccountKeys) TransactionError::InstructionError(0, InstructionError::NotEnoughAccountKeys)
); );
// too many accounts // too many accounts
let keyed_accounts = vec![ let content = vec![42; NUM_TO_SHARE];
KeyedAccount::new(&shared_key, true, &shared_account), let mut instruction_data = OFFSET.to_le_bytes().to_vec();
KeyedAccount::new(&shared_key, true, &shared_account), instruction_data.extend_from_slice(&content);
]; let mut transaction = Transaction::new_with_payer(
let mut input = serialize_parameters( &[Instruction::new_with_bytes(
&bpf_loader::id(), program_id,
&program_id, &instruction_data,
&keyed_accounts, vec![
&instruction_data, AccountMeta::new(shared_key, false),
) AccountMeta::new(shared_key, false),
.unwrap(); ],
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
let result = banks_client.process_transaction(transaction).await;
assert_eq!( assert_eq!(
unsafe { entrypoint(input.as_mut_ptr()) }, result.unwrap_err().unwrap(),
u64::from(ProgramError::InvalidArgument) TransactionError::InstructionError(0, InstructionError::InvalidArgument)
); );
// account data too small // account data too small
let keyed_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)];
let content = vec![42; NUM_TO_SHARE * 10]; let content = vec![42; NUM_TO_SHARE * 10];
let mut instruction_data = OFFSET.to_le_bytes().to_vec(); let mut instruction_data = OFFSET.to_le_bytes().to_vec();
instruction_data.extend_from_slice(&content); instruction_data.extend_from_slice(&content);
let mut input = serialize_parameters( let mut transaction = Transaction::new_with_payer(
&bpf_loader::id(), &[Instruction::new_with_bytes(
&program_id, program_id,
&keyed_accounts, &instruction_data,
&instruction_data, vec![AccountMeta::new(shared_key, false)],
) )],
.unwrap(); Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
let result = banks_client.process_transaction(transaction).await;
assert_eq!( assert_eq!(
unsafe { entrypoint(input.as_mut_ptr()) }, result.unwrap_err().unwrap(),
u64::from(ProgramError::AccountDataTooSmall) TransactionError::InstructionError(0, InstructionError::AccountDataTooSmall)
); );
// offset too large // offset too large
let keyed_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)];
let content = vec![42; NUM_TO_SHARE]; let content = vec![42; NUM_TO_SHARE];
let mut instruction_data = (OFFSET * 10).to_le_bytes().to_vec(); let mut instruction_data = (OFFSET * 10).to_le_bytes().to_vec();
instruction_data.extend_from_slice(&content); instruction_data.extend_from_slice(&content);
let mut input = serialize_parameters( let mut transaction = Transaction::new_with_payer(
&bpf_loader::id(), &[Instruction::new_with_bytes(
&program_id, program_id,
&keyed_accounts, &instruction_data,
&instruction_data, vec![AccountMeta::new(shared_key, false)],
) )],
.unwrap(); Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
let result = banks_client.process_transaction(transaction).await;
assert_eq!( assert_eq!(
unsafe { entrypoint(input.as_mut_ptr()) }, result.unwrap_err().unwrap(),
u64::from(ProgramError::AccountDataTooSmall) TransactionError::InstructionError(0, InstructionError::AccountDataTooSmall)
); );
} }

View File

@ -6,21 +6,22 @@ homepage = "https://spl.solana.com/stake-pool"
license = "Apache-2.0" license = "Apache-2.0"
name = "spl-stake-pool-cli" name = "spl-stake-pool-cli"
repository = "https://github.com/solana-labs/solana-program-library" repository = "https://github.com/solana-labs/solana-program-library"
version = "2.0.1" version = "0.1.0"
[dependencies] [dependencies]
borsh = "0.8" borsh = "0.8"
clap = "2.33.3" clap = "2.33.3"
serde_json = "1.0.62" serde_json = "1.0.62"
solana-account-decoder = "1.6.2" solana-account-decoder = "1.6.7"
solana-clap-utils = "1.6.2" solana-clap-utils = "1.6.7"
solana-cli-config = "1.6.2" solana-cli-config = "1.6.7"
solana-client = "1.6.2" solana-client = "1.6.7"
solana-logger = "1.6.2" solana-logger = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
solana-program = "1.6.2" solana-program = "1.6.7"
spl-stake-pool = { path="../program", features = [ "no-entrypoint" ] } spl-associated-token-account = { version = "1.0", path="../../associated-token-account/program", features = [ "no-entrypoint" ] }
spl-token = { path="../../token/program", features = [ "no-entrypoint" ] } spl-stake-pool = { version = "0.2", path="../program", features = [ "no-entrypoint" ] }
spl-token = { version = "3.1", path="../../token/program", features = [ "no-entrypoint" ] }
bs58 = "0.4.0" bs58 = "0.4.0"
bincode = "1.3.1" bincode = "1.3.1"
lazy_static = "1.4.0" lazy_static = "1.4.0"

View File

@ -18,17 +18,17 @@ use {
type Error = Box<dyn std::error::Error>; type Error = Box<dyn std::error::Error>;
pub(crate) fn get_stake_pool( pub fn get_stake_pool(
rpc_client: &RpcClient, rpc_client: &RpcClient,
pool_address: &Pubkey, stake_pool_address: &Pubkey,
) -> Result<StakePool, Error> { ) -> Result<StakePool, Error> {
let account_data = rpc_client.get_account_data(pool_address)?; let account_data = rpc_client.get_account_data(stake_pool_address)?;
let stake_pool = StakePool::try_from_slice(account_data.as_slice()) let stake_pool = StakePool::try_from_slice(account_data.as_slice())
.map_err(|err| format!("Invalid stake pool {}: {}", pool_address, err))?; .map_err(|err| format!("Invalid stake pool {}: {}", stake_pool_address, err))?;
Ok(stake_pool) Ok(stake_pool)
} }
pub(crate) fn get_validator_list( pub fn get_validator_list(
rpc_client: &RpcClient, rpc_client: &RpcClient,
validator_list_address: &Pubkey, validator_list_address: &Pubkey,
) -> Result<ValidatorList, Error> { ) -> Result<ValidatorList, Error> {
@ -38,7 +38,7 @@ pub(crate) fn get_validator_list(
Ok(validator_list) Ok(validator_list)
} }
pub(crate) fn get_token_account( pub fn get_token_account(
rpc_client: &RpcClient, rpc_client: &RpcClient,
token_account_address: &Pubkey, token_account_address: &Pubkey,
expected_token_mint: &Pubkey, expected_token_mint: &Pubkey,
@ -58,7 +58,7 @@ pub(crate) fn get_token_account(
} }
} }
pub(crate) fn get_token_mint( pub fn get_token_mint(
rpc_client: &RpcClient, rpc_client: &RpcClient,
token_mint_address: &Pubkey, token_mint_address: &Pubkey,
) -> Result<spl_token::state::Mint, Error> { ) -> Result<spl_token::state::Mint, Error> {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "spl-stake-pool" name = "spl-stake-pool"
version = "0.1.0" version = "0.2.0"
description = "Solana Program Library Stake Pool" description = "Solana Program Library Stake Pool"
authors = ["Solana Maintainers <maintainers@solana.foundation>"] authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library" repository = "https://github.com/solana-labs/solana-program-library"
@ -19,17 +19,17 @@ num-traits = "0.2"
num_enum = "0.5.1" num_enum = "0.5.1"
serde = "1.0.121" serde = "1.0.121"
serde_derive = "1.0.103" serde_derive = "1.0.103"
solana-program = "1.6.2" solana-program = "1.6.7"
spl-math = { path = "../../libraries/math", features = [ "no-entrypoint" ] } spl-math = { version = "0.1", path = "../../libraries/math", features = [ "no-entrypoint" ] }
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] } spl-token = { version = "3.1", path = "../../token/program", features = [ "no-entrypoint" ] }
thiserror = "1.0" thiserror = "1.0"
bincode = "1.3.1" bincode = "1.3.1"
[dev-dependencies] [dev-dependencies]
proptest = "0.10" proptest = "0.10"
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
solana-vote-program = "1.6.2" solana-vote-program = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -28,16 +28,16 @@ pub enum StakePoolError {
/// Token account is associated with the wrong mint. /// Token account is associated with the wrong mint.
#[error("WrongAccountMint")] #[error("WrongAccountMint")]
WrongAccountMint, WrongAccountMint,
/// Wrong pool owner account. /// Wrong pool manager account.
#[error("WrongOwner")] #[error("WrongManager")]
WrongOwner, WrongManager,
/// Required signature is missing. /// Required signature is missing.
#[error("SignatureMissing")] #[error("SignatureMissing")]
SignatureMissing, SignatureMissing,
/// Invalid validator stake list account. /// Invalid validator stake list account.
#[error("InvalidValidatorStakeList")] #[error("InvalidValidatorStakeList")]
InvalidValidatorStakeList, InvalidValidatorStakeList,
/// Invalid owner fee account. /// Invalid manager fee account.
#[error("InvalidFeeAccount")] #[error("InvalidFeeAccount")]
InvalidFeeAccount, InvalidFeeAccount,
@ -79,6 +79,15 @@ pub enum StakePoolError {
/// The size of the given validator stake list does match the expected amount /// The size of the given validator stake list does match the expected amount
#[error("UnexpectedValidatorListAccountSize")] #[error("UnexpectedValidatorListAccountSize")]
UnexpectedValidatorListAccountSize, UnexpectedValidatorListAccountSize,
/// Wrong pool staker account.
#[error("WrongStaker")]
WrongStaker,
/// Pool token supply is not zero on initialization
#[error("NonZeroPoolTokenSupply")]
NonZeroPoolTokenSupply,
/// The lamports in the validator stake account is not equal to the minimum
#[error("StakeLamportsNotEqualToMinimum")]
StakeLamportsNotEqualToMinimum,
} }
impl From<StakePoolError> for ProgramError { impl From<StakePoolError> for ProgramError {
fn from(e: StakePoolError) -> Self { fn from(e: StakePoolError) -> Self {

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,10 @@ pub mod entrypoint;
// Export current sdk types for downstream users building with a different sdk version // Export current sdk types for downstream users building with a different sdk version
pub use solana_program; pub use solana_program;
use solana_program::pubkey::Pubkey; use {
crate::stake_program::Meta,
solana_program::{native_token::LAMPORTS_PER_SOL, pubkey::Pubkey},
};
/// Seed for deposit authority seed /// Seed for deposit authority seed
const AUTHORITY_DEPOSIT: &[u8] = b"deposit"; const AUTHORITY_DEPOSIT: &[u8] = b"deposit";
@ -22,6 +25,30 @@ const AUTHORITY_DEPOSIT: &[u8] = b"deposit";
/// Seed for withdraw authority seed /// Seed for withdraw authority seed
const AUTHORITY_WITHDRAW: &[u8] = b"withdraw"; const AUTHORITY_WITHDRAW: &[u8] = b"withdraw";
/// Seed for transient stake account
const TRANSIENT_STAKE_SEED: &[u8] = b"transient";
/// Minimum amount of staked SOL required in a validator stake account to allow
/// for merges without a mismatch on credits observed
pub const MINIMUM_ACTIVE_STAKE: u64 = LAMPORTS_PER_SOL;
/// Maximum amount of validator stake accounts to update per
/// `UpdateValidatorListBalance` instruction, based on compute limits
pub const MAX_VALIDATORS_TO_UPDATE: usize = 10;
/// Get the stake amount under consideration when calculating pool token
/// conversions
pub fn minimum_stake_lamports(meta: &Meta) -> u64 {
meta.rent_exempt_reserve
.saturating_add(MINIMUM_ACTIVE_STAKE)
}
/// Get the stake amount under consideration when calculating pool token
/// conversions
pub fn minimum_reserve_lamports(meta: &Meta) -> u64 {
meta.rent_exempt_reserve.saturating_add(1)
}
/// Generates the deposit authority program address for the stake pool /// Generates the deposit authority program address for the stake pool
pub fn find_deposit_authority_program_address( pub fn find_deposit_authority_program_address(
program_id: &Pubkey, program_id: &Pubkey,
@ -59,4 +86,20 @@ pub fn find_stake_program_address(
) )
} }
/// Generates the stake program address for a validator's vote account
pub fn find_transient_stake_program_address(
program_id: &Pubkey,
vote_account_address: &Pubkey,
stake_pool_address: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
TRANSIENT_STAKE_SEED,
&vote_account_address.to_bytes()[..32],
&stake_pool_address.to_bytes()[..32],
],
program_id,
)
}
solana_program::declare_id!("poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj"); solana_program::declare_id!("poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj");

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,22 @@
//! FIXME copied from the solana stake program //! FIXME copied from the solana stake program
use serde_derive::{Deserialize, Serialize}; use {
use solana_program::{ borsh::{
clock::{Epoch, UnixTimestamp}, maybestd::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult},
instruction::{AccountMeta, Instruction}, BorshDeserialize, BorshSchema, BorshSerialize,
pubkey::Pubkey, },
stake_history::StakeHistory, serde_derive::{Deserialize, Serialize},
system_instruction, sysvar, solana_program::{
clock::{Epoch, UnixTimestamp},
instruction::{AccountMeta, Instruction},
msg,
program_error::ProgramError,
pubkey::Pubkey,
stake_history::StakeHistory,
system_instruction, sysvar,
},
std::str::FromStr,
}; };
use std::str::FromStr;
solana_program::declare_id!("Stake11111111111111111111111111111111111111"); solana_program::declare_id!("Stake11111111111111111111111111111111111111");
@ -125,8 +133,39 @@ pub enum StakeState {
RewardsPool, RewardsPool,
} }
impl BorshDeserialize for StakeState {
fn deserialize(buf: &mut &[u8]) -> IoResult<Self> {
let u: u32 = BorshDeserialize::deserialize(buf)?;
match u {
0 => Ok(StakeState::Uninitialized),
1 => {
let meta: Meta = BorshDeserialize::deserialize(buf)?;
Ok(StakeState::Initialized(meta))
}
2 => {
let meta: Meta = BorshDeserialize::deserialize(buf)?;
let stake: Stake = BorshDeserialize::deserialize(buf)?;
Ok(StakeState::Stake(meta, stake))
}
3 => Ok(StakeState::RewardsPool),
_ => Err(IoError::new(IoErrorKind::InvalidData, "Invalid enum value")),
}
}
}
/// FIXME copied from the stake program /// FIXME copied from the stake program
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] #[derive(
BorshSerialize,
BorshDeserialize,
BorshSchema,
Default,
Debug,
Serialize,
Deserialize,
PartialEq,
Clone,
Copy,
)]
pub struct Meta { pub struct Meta {
/// FIXME copied from the stake program /// FIXME copied from the stake program
pub rent_exempt_reserve: u64, pub rent_exempt_reserve: u64,
@ -137,7 +176,18 @@ pub struct Meta {
} }
/// FIXME copied from the stake program /// FIXME copied from the stake program
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy)] #[derive(
BorshSerialize,
BorshDeserialize,
BorshSchema,
Debug,
Default,
Serialize,
Deserialize,
PartialEq,
Clone,
Copy,
)]
pub struct Stake { pub struct Stake {
/// FIXME copied from the stake program /// FIXME copied from the stake program
pub delegation: Delegation, pub delegation: Delegation,
@ -146,7 +196,18 @@ pub struct Stake {
} }
/// FIXME copied from the stake program /// FIXME copied from the stake program
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy)] #[derive(
BorshSerialize,
BorshDeserialize,
BorshSchema,
Debug,
Default,
Serialize,
Deserialize,
PartialEq,
Clone,
Copy,
)]
pub struct Delegation { pub struct Delegation {
/// to whom the stake is delegated /// to whom the stake is delegated
pub voter_pubkey: Pubkey, pub voter_pubkey: Pubkey,
@ -161,7 +222,17 @@ pub struct Delegation {
} }
/// FIXME copied from the stake program /// FIXME copied from the stake program
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] #[derive(
BorshSerialize,
BorshDeserialize,
BorshSchema,
Debug,
Serialize,
Deserialize,
PartialEq,
Clone,
Copy,
)]
pub enum StakeAuthorize { pub enum StakeAuthorize {
/// FIXME copied from the stake program /// FIXME copied from the stake program
Staker, Staker,
@ -169,7 +240,18 @@ pub enum StakeAuthorize {
Withdrawer, Withdrawer,
} }
/// FIXME copied from the stake program /// FIXME copied from the stake program
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] #[derive(
BorshSerialize,
BorshDeserialize,
BorshSchema,
Default,
Debug,
Serialize,
Deserialize,
PartialEq,
Clone,
Copy,
)]
pub struct Authorized { pub struct Authorized {
/// FIXME copied from the stake program /// FIXME copied from the stake program
pub staker: Pubkey, pub staker: Pubkey,
@ -178,7 +260,18 @@ pub struct Authorized {
} }
/// FIXME copied from the stake program /// FIXME copied from the stake program
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] #[derive(
BorshSerialize,
BorshDeserialize,
BorshSchema,
Default,
Debug,
Serialize,
Deserialize,
PartialEq,
Clone,
Copy,
)]
pub struct Lockup { pub struct Lockup {
/// UnixTimestamp at which this stake will allow withdrawal, unless the /// UnixTimestamp at which this stake will allow withdrawal, unless the
/// transaction is signed by the custodian /// transaction is signed by the custodian
@ -200,6 +293,14 @@ impl StakeState {
_ => None, _ => None,
} }
} }
/// Get meta
pub fn meta(&self) -> Option<&Meta> {
match self {
StakeState::Initialized(meta) => Some(meta),
StakeState::Stake(meta, _) => Some(meta),
_ => None,
}
}
} }
/// FIXME copied from the stake program /// FIXME copied from the stake program
@ -397,6 +498,41 @@ impl Delegation {
} }
} }
/// FIXME copied from stake program
/// Checks if two active delegations are mergeable, required since we cannot recover
/// from a CPI error.
pub fn active_delegations_can_merge(
stake: &Delegation,
source: &Delegation,
) -> Result<(), ProgramError> {
if stake.voter_pubkey != source.voter_pubkey {
msg!("Unable to merge due to voter mismatch");
Err(ProgramError::InvalidAccountData)
} else if (stake.warmup_cooldown_rate - source.warmup_cooldown_rate).abs() < f64::EPSILON
&& stake.deactivation_epoch == Epoch::MAX
&& source.deactivation_epoch == Epoch::MAX
{
Ok(())
} else {
msg!("Unable to merge due to stake deactivation");
Err(ProgramError::InvalidAccountData)
}
}
/// FIXME copied from stake program
/// Checks if two active stakes are mergeable, required since we cannot recover
/// from a CPI error.
pub fn active_stakes_can_merge(stake: &Stake, source: &Stake) -> Result<(), ProgramError> {
active_delegations_can_merge(&stake.delegation, &source.delegation)?;
if stake.credits_observed == source.credits_observed {
Ok(())
} else {
msg!("Unable to merge due to credits observed mismatch");
Err(ProgramError::InvalidAccountData)
}
}
/// FIXME copied from the stake program /// FIXME copied from the stake program
pub fn split_only( pub fn split_only(
stake_pubkey: &Pubkey, stake_pubkey: &Pubkey,
@ -508,3 +644,64 @@ pub fn deactivate_stake(stake_pubkey: &Pubkey, authorized_pubkey: &Pubkey) -> In
]; ];
Instruction::new_with_bincode(id(), &StakeInstruction::Deactivate, account_metas) Instruction::new_with_bincode(id(), &StakeInstruction::Deactivate, account_metas)
} }
#[cfg(test)]
mod test {
use {super::*, crate::borsh::try_from_slice_unchecked, bincode::serialize};
fn check_borsh_deserialization(stake: StakeState) {
let serialized = serialize(&stake).unwrap();
let deserialized = StakeState::try_from_slice(&serialized).unwrap();
assert_eq!(stake, deserialized);
}
#[test]
fn bincode_vs_borsh() {
check_borsh_deserialization(StakeState::Uninitialized);
check_borsh_deserialization(StakeState::RewardsPool);
check_borsh_deserialization(StakeState::Initialized(Meta {
rent_exempt_reserve: u64::MAX,
authorized: Authorized {
staker: Pubkey::new_unique(),
withdrawer: Pubkey::new_unique(),
},
lockup: Lockup::default(),
}));
check_borsh_deserialization(StakeState::Stake(
Meta {
rent_exempt_reserve: 1,
authorized: Authorized {
staker: Pubkey::new_unique(),
withdrawer: Pubkey::new_unique(),
},
lockup: Lockup::default(),
},
Stake {
delegation: Delegation {
voter_pubkey: Pubkey::new_unique(),
stake: u64::MAX,
activation_epoch: Epoch::MAX,
deactivation_epoch: Epoch::MAX,
warmup_cooldown_rate: f64::MAX,
},
credits_observed: 1,
},
));
}
#[test]
fn borsh_deserialization_live_data() {
let data = [
1, 0, 0, 0, 128, 213, 34, 0, 0, 0, 0, 0, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35,
119, 124, 168, 12, 120, 216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149,
224, 109, 52, 100, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35, 119, 124, 168, 12, 120,
216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149, 224, 109, 52, 100, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
let _deserialized = try_from_slice_unchecked::<StakeState>(&data).unwrap();
}
}

View File

@ -1,9 +1,9 @@
//! State transition types //! State transition types
use { use {
crate::{error::StakePoolError, instruction::Fee}, crate::error::StakePoolError,
borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}, solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey},
spl_math::checked_ceil_div::CheckedCeilDiv, spl_math::checked_ceil_div::CheckedCeilDiv,
std::convert::TryFrom, std::convert::TryFrom,
}; };
@ -31,66 +31,94 @@ impl Default for AccountType {
pub struct StakePool { pub struct StakePool {
/// Account type, must be StakePool currently /// Account type, must be StakePool currently
pub account_type: AccountType, pub account_type: AccountType,
/// Owner authority
/// allows for updating the staking authority /// Manager authority, allows for updating the staker, manager, and fee account
pub owner: Pubkey, pub manager: Pubkey,
/// Deposit authority bump seed
/// for `create_program_address(&[state::StakePool account, "deposit"])` /// Staker authority, allows for adding and removing validators, and managing stake
pub deposit_bump_seed: u8, /// distribution
pub staker: Pubkey,
/// Deposit authority
///
/// If a depositor pubkey is specified on initialization, then deposits must be
/// signed by this authority. If no deposit authority is specified,
/// then the stake pool will default to the result of:
/// `Pubkey::find_program_address(
/// &[&stake_pool_address.to_bytes()[..32], b"deposit"],
/// program_id,
/// )`
pub deposit_authority: Pubkey,
/// Withdrawal authority bump seed /// Withdrawal authority bump seed
/// for `create_program_address(&[state::StakePool account, "withdrawal"])` /// for `create_program_address(&[state::StakePool account, "withdrawal"])`
pub withdraw_bump_seed: u8, pub withdraw_bump_seed: u8,
/// Validator stake list storage account /// Validator stake list storage account
pub validator_list: Pubkey, pub validator_list: Pubkey,
/// Reserve stake account, holds deactivated stake
pub reserve_stake: Pubkey,
/// Pool Mint /// Pool Mint
pub pool_mint: Pubkey, pub pool_mint: Pubkey,
/// Owner fee account
pub owner_fee_account: Pubkey, /// Manager fee account
pub manager_fee_account: Pubkey,
/// Pool token program id /// Pool token program id
pub token_program_id: Pubkey, pub token_program_id: Pubkey,
/// total stake under management
pub stake_total: u64, /// Total stake under management.
/// total pool /// Note that if `last_update_epoch` does not match the current epoch then
pub pool_total: u64, /// this field may not be accurate
/// Last epoch stake_total field was updated pub total_stake_lamports: u64,
/// Total supply of pool tokens (should always match the supply in the Pool Mint)
pub pool_token_supply: u64,
/// Last epoch the `total_stake_lamports` field was updated
pub last_update_epoch: u64, pub last_update_epoch: u64,
/// Fee applied to deposits /// Fee applied to deposits
pub fee: Fee, pub fee: Fee,
} }
impl StakePool { impl StakePool {
/// calculate the pool tokens that should be minted /// calculate the pool tokens that should be minted for a deposit of `stake_lamports`
pub fn calc_pool_deposit_amount(&self, stake_lamports: u64) -> Option<u64> { pub fn calc_pool_tokens_for_deposit(&self, stake_lamports: u64) -> Option<u64> {
if self.stake_total == 0 { if self.total_stake_lamports == 0 || self.pool_token_supply == 0 {
return Some(stake_lamports); return Some(stake_lamports);
} }
u64::try_from( u64::try_from(
(stake_lamports as u128) (stake_lamports as u128)
.checked_mul(self.pool_total as u128)? .checked_mul(self.pool_token_supply as u128)?
.checked_div(self.stake_total as u128)?, .checked_div(self.total_stake_lamports as u128)?,
) )
.ok() .ok()
} }
/// calculate the pool tokens that should be withdrawn /// calculate the pool tokens that should be burned for a withdrawal of `stake_lamports`
pub fn calc_pool_withdraw_amount(&self, stake_lamports: u64) -> Option<u64> { pub fn calc_pool_tokens_for_withdraw(&self, stake_lamports: u64) -> Option<u64> {
let (quotient, _) = (stake_lamports as u128) let (quotient, _) = (stake_lamports as u128)
.checked_mul(self.pool_total as u128)? .checked_mul(self.pool_token_supply as u128)?
.checked_ceil_div(self.stake_total as u128)?; .checked_ceil_div(self.total_stake_lamports as u128)?;
u64::try_from(quotient).ok() u64::try_from(quotient).ok()
} }
/// calculate lamports amount on withdrawal /// calculate lamports amount on withdrawal
pub fn calc_lamports_withdraw_amount(&self, pool_tokens: u64) -> Option<u64> { pub fn calc_lamports_withdraw_amount(&self, pool_tokens: u64) -> Option<u64> {
u64::try_from( u64::try_from(
(pool_tokens as u128) (pool_tokens as u128)
.checked_mul(self.stake_total as u128)? .checked_mul(self.total_stake_lamports as u128)?
.checked_div(self.pool_total as u128)?, .checked_div(self.pool_token_supply as u128)?,
) )
.ok() .ok()
} }
/// calculate the fee in pool tokens that goes to the owner /// calculate the fee in pool tokens that goes to the manager
pub fn calc_fee_amount(&self, pool_amount: u64) -> Option<u64> { pub fn calc_fee_amount(&self, reward_lamports: u64) -> Option<u64> {
if self.fee.denominator == 0 { if self.fee.denominator == 0 {
return Some(0); return Some(0);
} }
let pool_amount = self.calc_pool_tokens_for_deposit(reward_lamports)?;
u64::try_from( u64::try_from(
(pool_amount as u128) (pool_amount as u128)
.checked_mul(self.fee.numerator as u128)? .checked_mul(self.fee.numerator as u128)?
@ -107,18 +135,23 @@ impl StakePool {
authority_seed: &[u8], authority_seed: &[u8],
bump_seed: u8, bump_seed: u8,
) -> Result<(), ProgramError> { ) -> Result<(), ProgramError> {
if *authority_address let expected_address = Pubkey::create_program_address(
== Pubkey::create_program_address( &[
&[ &stake_pool_address.to_bytes()[..32],
&stake_pool_address.to_bytes()[..32], authority_seed,
authority_seed, &[bump_seed],
&[bump_seed], ],
], program_id,
program_id, )?;
)?
{ if *authority_address == expected_address {
Ok(()) Ok(())
} else { } else {
msg!(
"Incorrect authority provided, expected {}, received {}",
expected_address,
authority_address
);
Err(StakePoolError::InvalidProgramAddress.into()) Err(StakePoolError::InvalidProgramAddress.into())
} }
} }
@ -139,32 +172,94 @@ impl StakePool {
) )
} }
/// Checks that the deposit authority is valid /// Checks that the deposit authority is valid
pub(crate) fn check_authority_deposit( pub(crate) fn check_deposit_authority(
&self, &self,
deposit_authority: &Pubkey, deposit_authority: &Pubkey,
program_id: &Pubkey,
stake_pool_address: &Pubkey,
) -> Result<(), ProgramError> { ) -> Result<(), ProgramError> {
Self::check_authority( if self.deposit_authority == *deposit_authority {
deposit_authority, Ok(())
program_id, } else {
stake_pool_address, Err(StakePoolError::InvalidProgramAddress.into())
crate::AUTHORITY_DEPOSIT, }
self.deposit_bump_seed,
)
} }
/// Check owner validity and signature /// Check staker validity and signature
pub(crate) fn check_owner(&self, owner_info: &AccountInfo) -> Result<(), ProgramError> { pub(crate) fn check_mint(&self, mint_info: &AccountInfo) -> Result<(), ProgramError> {
if *owner_info.key != self.owner { if *mint_info.key != self.pool_mint {
return Err(StakePoolError::WrongOwner.into()); Err(StakePoolError::WrongPoolMint.into())
} else {
Ok(())
} }
if !owner_info.is_signer { }
/// Check manager validity and signature
pub(crate) fn check_manager(&self, manager_info: &AccountInfo) -> Result<(), ProgramError> {
if *manager_info.key != self.manager {
msg!(
"Incorrect manager provided, expected {}, received {}",
self.manager,
manager_info.key
);
return Err(StakePoolError::WrongManager.into());
}
if !manager_info.is_signer {
msg!("Manager signature missing");
return Err(StakePoolError::SignatureMissing.into()); return Err(StakePoolError::SignatureMissing.into());
} }
Ok(()) Ok(())
} }
/// Check staker validity and signature
pub(crate) fn check_staker(&self, staker_info: &AccountInfo) -> Result<(), ProgramError> {
if *staker_info.key != self.staker {
msg!(
"Incorrect staker provided, expected {}, received {}",
self.staker,
staker_info.key
);
return Err(StakePoolError::WrongStaker.into());
}
if !staker_info.is_signer {
msg!("Staker signature missing");
return Err(StakePoolError::SignatureMissing.into());
}
Ok(())
}
/// Check the validator list is valid
pub fn check_validator_list(
&self,
validator_list_info: &AccountInfo,
) -> Result<(), ProgramError> {
if *validator_list_info.key != self.validator_list {
msg!(
"Invalid validator list provided, expected {}, received {}",
self.validator_list,
validator_list_info.key
);
Err(StakePoolError::InvalidValidatorStakeList.into())
} else {
Ok(())
}
}
/// Check the validator list is valid
pub fn check_reserve_stake(
&self,
reserve_stake_info: &AccountInfo,
) -> Result<(), ProgramError> {
if *reserve_stake_info.key != self.reserve_stake {
msg!(
"Invalid reserve stake provided, expected {}, received {}",
self.reserve_stake,
reserve_stake_info.key
);
Err(StakePoolError::InvalidProgramAddress.into())
} else {
Ok(())
}
}
/// Check if StakePool is actually initialized as a stake pool /// Check if StakePool is actually initialized as a stake pool
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
self.account_type == AccountType::StakePool self.account_type == AccountType::StakePool
@ -186,21 +281,45 @@ pub struct ValidatorList {
/// Maximum allowable number of validators /// Maximum allowable number of validators
pub max_validators: u32, pub max_validators: u32,
/// List of all validator stake accounts and their info /// List of stake info for each validator in the pool
pub validators: Vec<ValidatorStakeInfo>, pub validators: Vec<ValidatorStakeInfo>,
} }
/// Status of the stake account in the validator list, for accounting
#[derive(Copy, Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum StakeStatus {
/// Stake account is active, there may be a transient stake as well
Active,
/// Only transient stake account exists, when a transient stake is
/// deactivating during validator removal
DeactivatingTransient,
/// No more validator stake accounts exist, entry ready for removal during
/// `UpdateStakePoolBalance`
ReadyForRemoval,
}
impl Default for StakeStatus {
fn default() -> Self {
Self::Active
}
}
/// Information about the singe validator stake account /// Information about the singe validator stake account
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] #[derive(Clone, Copy, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct ValidatorStakeInfo { pub struct ValidatorStakeInfo {
/// Status of the validator stake account
pub status: StakeStatus,
/// Validator vote account address /// Validator vote account address
pub vote_account: Pubkey, pub vote_account_address: Pubkey,
/// Balance of the validator's stake account /// Amount of stake delegated to this validator
pub balance: u64, /// Note that if `last_update_epoch` does not match the current epoch then this field may not
/// be accurate
pub stake_lamports: u64,
/// Last epoch balance field was updated /// Last epoch the `stake_lamports` field was updated
pub last_update_epoch: u64, pub last_update_epoch: u64,
} }
@ -217,27 +336,27 @@ impl ValidatorList {
/// Calculate the number of validator entries that fit in the provided length /// Calculate the number of validator entries that fit in the provided length
pub fn calculate_max_validators(buffer_length: usize) -> usize { pub fn calculate_max_validators(buffer_length: usize) -> usize {
let header_size = 1 + 4 + 4; let header_size = 1 + 4 + 4;
buffer_length.saturating_sub(header_size) / 48 buffer_length.saturating_sub(header_size) / 49
} }
/// Check if contains validator with particular pubkey /// Check if contains validator with particular pubkey
pub fn contains(&self, vote_account: &Pubkey) -> bool { pub fn contains(&self, vote_account_address: &Pubkey) -> bool {
self.validators self.validators
.iter() .iter()
.any(|x| x.vote_account == *vote_account) .any(|x| x.vote_account_address == *vote_account_address)
} }
/// Check if contains validator with particular pubkey /// Check if contains validator with particular pubkey
pub fn find_mut(&mut self, vote_account: &Pubkey) -> Option<&mut ValidatorStakeInfo> { pub fn find_mut(&mut self, vote_account_address: &Pubkey) -> Option<&mut ValidatorStakeInfo> {
self.validators self.validators
.iter_mut() .iter_mut()
.find(|x| x.vote_account == *vote_account) .find(|x| x.vote_account_address == *vote_account_address)
} }
/// Check if contains validator with particular pubkey /// Check if contains validator with particular pubkey
pub fn find(&self, vote_account: &Pubkey) -> Option<&ValidatorStakeInfo> { pub fn find(&self, vote_account_address: &Pubkey) -> Option<&ValidatorStakeInfo> {
self.validators self.validators
.iter() .iter()
.find(|x| x.vote_account == *vote_account) .find(|x| x.vote_account_address == *vote_account_address)
} }
/// Check if validator stake list is actually initialized as a validator stake list /// Check if validator stake list is actually initialized as a validator stake list
@ -251,6 +370,17 @@ impl ValidatorList {
} }
} }
/// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of
/// the rewards
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
pub struct Fee {
/// denominator of the fee ratio
pub denominator: u64,
/// numerator of the fee ratio
pub numerator: u64,
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use { use {
@ -294,18 +424,21 @@ mod test {
max_validators, max_validators,
validators: vec![ validators: vec![
ValidatorStakeInfo { ValidatorStakeInfo {
vote_account: Pubkey::new_from_array([1; 32]), status: StakeStatus::Active,
balance: 123456789, vote_account_address: Pubkey::new_from_array([1; 32]),
stake_lamports: 123456789,
last_update_epoch: 987654321, last_update_epoch: 987654321,
}, },
ValidatorStakeInfo { ValidatorStakeInfo {
vote_account: Pubkey::new_from_array([2; 32]), status: StakeStatus::DeactivatingTransient,
balance: 998877665544, vote_account_address: Pubkey::new_from_array([2; 32]),
stake_lamports: 998877665544,
last_update_epoch: 11223445566, last_update_epoch: 11223445566,
}, },
ValidatorStakeInfo { ValidatorStakeInfo {
vote_account: Pubkey::new_from_array([3; 32]), status: StakeStatus::ReadyForRemoval,
balance: 0, vote_account_address: Pubkey::new_from_array([3; 32]),
stake_lamports: 0,
last_update_epoch: 999999999999999, last_update_epoch: 999999999999999,
}, },
], ],

View File

@ -0,0 +1,386 @@
#![cfg(feature = "test-bpf")]
mod helpers;
use {
bincode::deserialize,
helpers::*,
solana_program::{
clock::Epoch, hash::Hash, instruction::InstructionError, pubkey::Pubkey,
system_instruction::SystemError,
},
solana_program_test::*,
solana_sdk::{
signature::{Keypair, Signer},
transaction::{Transaction, TransactionError},
},
spl_stake_pool::{error::StakePoolError, id, instruction, stake_program},
};
async fn setup() -> (
BanksClient,
Keypair,
Hash,
StakePoolAccounts,
ValidatorStakeAccount,
DepositStakeAccount,
u64,
) {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let deposit_info = simple_deposit(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&validator_stake_account,
100_000_000,
)
.await
.unwrap();
let lamports = deposit_info.stake_lamports / 2;
(
banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake_account,
deposit_info,
lamports,
)
}
#[tokio::test]
async fn success() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
_deposit_info,
decrease_lamports,
) = setup().await;
// Save validator stake
let pre_validator_stake_account =
get_account(&mut banks_client, &validator_stake.stake_account).await;
// Check no transient stake
let transient_account = banks_client
.get_account(validator_stake.transient_stake_account)
.await
.unwrap();
assert!(transient_account.is_none());
let error = stake_pool_accounts
.decrease_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
decrease_lamports,
)
.await;
assert!(error.is_none());
// Check validator stake account balance
let validator_stake_account =
get_account(&mut banks_client, &validator_stake.stake_account).await;
let validator_stake_state =
deserialize::<stake_program::StakeState>(&validator_stake_account.data).unwrap();
assert_eq!(
pre_validator_stake_account.lamports - decrease_lamports,
validator_stake_account.lamports
);
assert_eq!(
validator_stake_state
.delegation()
.unwrap()
.deactivation_epoch,
Epoch::MAX
);
// Check transient stake account state and balance
let transient_stake_account =
get_account(&mut banks_client, &validator_stake.transient_stake_account).await;
let transient_stake_state =
deserialize::<stake_program::StakeState>(&transient_stake_account.data).unwrap();
assert_eq!(transient_stake_account.lamports, decrease_lamports);
assert_ne!(
transient_stake_state
.delegation()
.unwrap()
.deactivation_epoch,
Epoch::MAX
);
}
#[tokio::test]
async fn fail_with_wrong_withdraw_authority() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
_deposit_info,
decrease_lamports,
) = setup().await;
let wrong_authority = Pubkey::new_unique();
let transaction = Transaction::new_signed_with_payer(
&[instruction::decrease_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&wrong_authority,
&stake_pool_accounts.validator_list.pubkey(),
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
decrease_lamports,
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.staker],
recent_blockhash,
);
let error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakePoolError::InvalidProgramAddress as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while decreasing with wrong withdraw authority"),
}
}
#[tokio::test]
async fn fail_with_wrong_validator_list() {
let (
mut banks_client,
payer,
recent_blockhash,
mut stake_pool_accounts,
validator_stake,
_deposit_info,
decrease_lamports,
) = setup().await;
stake_pool_accounts.validator_list = Keypair::new();
let transaction = Transaction::new_signed_with_payer(
&[instruction::decrease_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.withdraw_authority,
&stake_pool_accounts.validator_list.pubkey(),
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
decrease_lamports,
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.staker],
recent_blockhash,
);
let error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while decreasing with wrong validator stake list account"),
}
}
#[tokio::test]
async fn fail_with_unknown_validator() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
_validator_stake,
_deposit_info,
decrease_lamports,
) = setup().await;
let unknown_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
unknown_stake
.create_and_delegate(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.staker,
)
.await;
let transaction = Transaction::new_signed_with_payer(
&[instruction::decrease_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.withdraw_authority,
&stake_pool_accounts.validator_list.pubkey(),
&unknown_stake.stake_account,
&unknown_stake.transient_stake_account,
decrease_lamports,
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.staker],
recent_blockhash,
);
let error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakePoolError::ValidatorNotFound as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while decreasing stake from unknown validator"),
}
}
#[tokio::test]
async fn fail_decrease_twice() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
_deposit_info,
decrease_lamports,
) = setup().await;
let error = stake_pool_accounts
.decrease_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
decrease_lamports / 3,
)
.await;
assert!(error.is_none());
let error = stake_pool_accounts
.decrease_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
decrease_lamports / 2,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = SystemError::AccountAlreadyInUse as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error"),
}
}
#[tokio::test]
async fn fail_with_small_lamport_amount() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
_deposit_info,
_decrease_lamports,
) = setup().await;
let rent = banks_client.get_rent().await.unwrap();
let lamports = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
let error = stake_pool_accounts
.decrease_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
lamports,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::InvalidError) => {}
_ => panic!("Wrong error occurs while try to decrease small stake"),
}
}
#[tokio::test]
async fn fail_overdraw_validator() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
deposit_info,
_decrease_lamports,
) = setup().await;
let error = stake_pool_accounts
.decrease_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
deposit_info.stake_lamports * 1_000_000,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::InsufficientFunds) => {}
_ => panic!("Wrong error occurs while overdrawing stake account on decrease"),
}
}

View File

@ -3,6 +3,7 @@
mod helpers; mod helpers;
use { use {
bincode::deserialize,
borsh::{BorshDeserialize, BorshSerialize}, borsh::{BorshDeserialize, BorshSerialize},
helpers::*, helpers::*,
solana_program::{ solana_program::{
@ -19,7 +20,8 @@ use {
transport::TransportError, transport::TransportError,
}, },
spl_stake_pool::{ spl_stake_pool::{
borsh::try_from_slice_unchecked, error, id, instruction, stake_program, state, borsh::try_from_slice_unchecked, error, id, instruction, minimum_stake_lamports,
stake_program, state,
}, },
spl_token::error as token_error, spl_token::error as token_error,
}; };
@ -34,7 +36,7 @@ async fn setup() -> (
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
@ -78,6 +80,7 @@ async fn test_stake_pool_deposit() {
&user_stake, &user_stake,
&authorized, &authorized,
&lockup, &lockup,
TEST_STAKE_AMOUNT,
) )
.await; .await;
@ -85,6 +88,7 @@ async fn test_stake_pool_deposit() {
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&validator_stake_account.validator,
&validator_stake_account.vote, &validator_stake_account.vote,
) )
.await; .await;
@ -98,28 +102,6 @@ async fn test_stake_pool_deposit() {
) )
.await; .await;
// Change authority to the stake pool's deposit
authorize_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&stake_authority,
&stake_pool_accounts.deposit_authority,
stake_program::StakeAuthorize::Withdrawer,
)
.await;
authorize_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&stake_authority,
&stake_pool_accounts.deposit_authority,
stake_program::StakeAuthorize::Staker,
)
.await;
// make pool token account // make pool token account
let user_pool_account = Keypair::new(); let user_pool_account = Keypair::new();
create_token_account( create_token_account(
@ -159,6 +141,7 @@ async fn test_stake_pool_deposit() {
&user_stake.pubkey(), &user_stake.pubkey(),
&user_pool_account.pubkey(), &user_pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&stake_authority,
) )
.await .await
.unwrap(); .unwrap();
@ -171,30 +154,23 @@ async fn test_stake_pool_deposit() {
.is_none()); .is_none());
let tokens_issued = stake_lamports; // For now tokens are 1:1 to stake let tokens_issued = stake_lamports; // For now tokens are 1:1 to stake
let fee = stake_pool_accounts.calculate_fee(tokens_issued);
// Stake pool should add its balance to the pool balance // Stake pool should add its balance to the pool balance
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap(); let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!( assert_eq!(
stake_pool.stake_total, stake_pool.total_stake_lamports,
stake_pool_before.stake_total + stake_lamports stake_pool_before.total_stake_lamports + stake_lamports
); );
assert_eq!( assert_eq!(
stake_pool.pool_total, stake_pool.pool_token_supply,
stake_pool_before.pool_total + tokens_issued stake_pool_before.pool_token_supply + tokens_issued
); );
// Check minted tokens // Check minted tokens
let user_token_balance = let user_token_balance =
get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await; get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
assert_eq!(user_token_balance, tokens_issued - fee); assert_eq!(user_token_balance, tokens_issued);
let pool_fee_token_balance = get_token_balance(
&mut banks_client,
&stake_pool_accounts.pool_fee_account.pubkey(),
)
.await;
assert_eq!(pool_fee_token_balance, fee);
// Check balances in validator stake account list storage // Check balances in validator stake account list storage
let validator_list = get_account( let validator_list = get_account(
@ -208,16 +184,19 @@ async fn test_stake_pool_deposit() {
.find(&validator_stake_account.vote.pubkey()) .find(&validator_stake_account.vote.pubkey())
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
validator_stake_item.balance, validator_stake_item.stake_lamports,
validator_stake_item_before.balance + stake_lamports validator_stake_item_before.stake_lamports + stake_lamports
); );
// Check validator stake account actual SOL balance // Check validator stake account actual SOL balance
let validator_stake_account = let validator_stake_account =
get_account(&mut banks_client, &validator_stake_account.stake_account).await; get_account(&mut banks_client, &validator_stake_account.stake_account).await;
let stake_state =
deserialize::<stake_program::StakeState>(&validator_stake_account.data).unwrap();
let meta = stake_state.meta().unwrap();
assert_eq!( assert_eq!(
validator_stake_account.lamports, validator_stake_account.lamports - minimum_stake_lamports(&meta),
validator_stake_item.balance validator_stake_item.stake_lamports
); );
} }
@ -253,7 +232,6 @@ async fn test_stake_pool_deposit_with_wrong_stake_program_id() {
AccountMeta::new(user_stake.pubkey(), false), AccountMeta::new(user_stake.pubkey(), false),
AccountMeta::new(validator_stake_account.stake_account, false), AccountMeta::new(validator_stake_account.stake_account, false),
AccountMeta::new(user_pool_account.pubkey(), false), AccountMeta::new(user_pool_account.pubkey(), false),
AccountMeta::new(stake_pool_accounts.pool_fee_account.pubkey(), false),
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false), AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(sysvar::stake_history::id(), false), AccountMeta::new_readonly(sysvar::stake_history::id(), false),
@ -284,75 +262,6 @@ async fn test_stake_pool_deposit_with_wrong_stake_program_id() {
} }
} }
#[tokio::test]
async fn test_stake_pool_deposit_with_wrong_pool_fee_account() {
let (
mut banks_client,
payer,
recent_blockhash,
mut stake_pool_accounts,
validator_stake_account,
) = setup().await;
let user = Keypair::new();
// make stake account
let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized {
staker: stake_pool_accounts.deposit_authority,
withdrawer: stake_pool_accounts.deposit_authority,
};
create_independent_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake,
&authorized,
&lockup,
)
.await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
let wrong_pool_fee_acc = Keypair::new();
stake_pool_accounts.pool_fee_account = wrong_pool_fee_acc;
let transaction_error = stake_pool_accounts
.deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&user_pool_account.pubkey(),
&validator_stake_account.stake_account,
)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = error::StakePoolError::InvalidFeeAccount as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to make a deposit with wrong pool fee account"),
}
}
#[tokio::test] #[tokio::test]
async fn test_stake_pool_deposit_with_wrong_token_program_id() { async fn test_stake_pool_deposit_with_wrong_token_program_id() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) = let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) =
@ -363,8 +272,8 @@ async fn test_stake_pool_deposit_with_wrong_token_program_id() {
let user_stake = Keypair::new(); let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default(); let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized { let authorized = stake_program::Authorized {
staker: stake_pool_accounts.deposit_authority, staker: user.pubkey(),
withdrawer: stake_pool_accounts.deposit_authority, withdrawer: user.pubkey(),
}; };
create_independent_stake_account( create_independent_stake_account(
&mut banks_client, &mut banks_client,
@ -373,6 +282,7 @@ async fn test_stake_pool_deposit_with_wrong_token_program_id() {
&user_stake, &user_stake,
&authorized, &authorized,
&lockup, &lockup,
TEST_STAKE_AMOUNT,
) )
.await; .await;
@ -392,23 +302,21 @@ async fn test_stake_pool_deposit_with_wrong_token_program_id() {
let wrong_token_program = Keypair::new(); let wrong_token_program = Keypair::new();
let mut transaction = Transaction::new_with_payer( let mut transaction = Transaction::new_with_payer(
&[instruction::deposit( &instruction::deposit(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.validator_list.pubkey(), &stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.withdraw_authority, &stake_pool_accounts.withdraw_authority,
&user_stake.pubkey(), &user_stake.pubkey(),
&user.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&user_pool_account.pubkey(), &user_pool_account.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&wrong_token_program.pubkey(), &wrong_token_program.pubkey(),
) ),
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer], recent_blockhash); transaction.sign(&[&payer, &user], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -438,8 +346,8 @@ async fn test_stake_pool_deposit_with_wrong_validator_list_account() {
let user_stake = Keypair::new(); let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default(); let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized { let authorized = stake_program::Authorized {
staker: stake_pool_accounts.deposit_authority, staker: user.pubkey(),
withdrawer: stake_pool_accounts.deposit_authority, withdrawer: user.pubkey(),
}; };
create_independent_stake_account( create_independent_stake_account(
&mut banks_client, &mut banks_client,
@ -448,6 +356,7 @@ async fn test_stake_pool_deposit_with_wrong_validator_list_account() {
&user_stake, &user_stake,
&authorized, &authorized,
&lockup, &lockup,
TEST_STAKE_AMOUNT,
) )
.await; .await;
@ -475,6 +384,7 @@ async fn test_stake_pool_deposit_with_wrong_validator_list_account() {
&user_stake.pubkey(), &user_stake.pubkey(),
&user_pool_account.pubkey(), &user_pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&user,
) )
.await .await
.err() .err()
@ -497,20 +407,18 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let validator_stake_account = ValidatorStakeAccount::new_with_target_authority( let validator_stake_account =
&stake_pool_accounts.deposit_authority, ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
&stake_pool_accounts.stake_pool.pubkey(),
);
validator_stake_account validator_stake_account
.create_and_delegate( .create_and_delegate(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.owner, &stake_pool_accounts.staker,
) )
.await; .await;
@ -531,8 +439,8 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
let user_stake = Keypair::new(); let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default(); let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized { let authorized = stake_program::Authorized {
staker: stake_pool_accounts.deposit_authority, staker: user.pubkey(),
withdrawer: stake_pool_accounts.deposit_authority, withdrawer: user.pubkey(),
}; };
create_independent_stake_account( create_independent_stake_account(
&mut banks_client, &mut banks_client,
@ -541,6 +449,7 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
&user_stake, &user_stake,
&authorized, &authorized,
&lockup, &lockup,
TEST_STAKE_AMOUNT,
) )
.await; .await;
@ -552,6 +461,7 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
&user_stake.pubkey(), &user_stake.pubkey(),
&user_pool_account.pubkey(), &user_pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&user,
) )
.await .await
.err() .err()
@ -571,74 +481,6 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
} }
} }
#[tokio::test]
async fn test_stake_pool_deposit_with_wrong_deposit_authority() {
let (
mut banks_client,
payer,
recent_blockhash,
mut stake_pool_accounts,
validator_stake_account,
) = setup().await;
let user = Keypair::new();
// make stake account
let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized {
staker: stake_pool_accounts.deposit_authority,
withdrawer: stake_pool_accounts.deposit_authority,
};
create_independent_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake,
&authorized,
&lockup,
)
.await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
stake_pool_accounts.deposit_authority = Keypair::new().pubkey();
let transaction_error = stake_pool_accounts
.deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&user_pool_account.pubkey(),
&validator_stake_account.stake_account,
)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = error::StakePoolError::InvalidProgramAddress as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to make a deposit with wrong deposit authority"),
}
}
#[tokio::test] #[tokio::test]
async fn test_stake_pool_deposit_with_wrong_withdraw_authority() { async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
let ( let (
@ -654,8 +496,8 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
let user_stake = Keypair::new(); let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default(); let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized { let authorized = stake_program::Authorized {
staker: stake_pool_accounts.deposit_authority, staker: user.pubkey(),
withdrawer: stake_pool_accounts.deposit_authority, withdrawer: user.pubkey(),
}; };
create_independent_stake_account( create_independent_stake_account(
&mut banks_client, &mut banks_client,
@ -664,6 +506,7 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
&user_stake, &user_stake,
&authorized, &authorized,
&lockup, &lockup,
TEST_STAKE_AMOUNT,
) )
.await; .await;
@ -690,6 +533,7 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
&user_stake.pubkey(), &user_stake.pubkey(),
&user_pool_account.pubkey(), &user_pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&user,
) )
.await .await
.err() .err()
@ -707,75 +551,18 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
} }
} }
#[tokio::test]
async fn test_stake_pool_deposit_with_wrong_set_deposit_authority() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) =
setup().await;
let user = Keypair::new();
// make stake account
let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized {
staker: Keypair::new().pubkey(),
withdrawer: stake_pool_accounts.deposit_authority,
};
create_independent_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake,
&authorized,
&lockup,
)
.await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
let transaction_error = stake_pool_accounts
.deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&user_pool_account.pubkey(),
&validator_stake_account.stake_account,
)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
assert_eq!(error, InstructionError::MissingRequiredSignature);
}
_ => {
panic!("Wrong error occurs while try to make deposit with wrong set deposit authority")
}
}
}
#[tokio::test] #[tokio::test]
async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() { async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) = let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) =
setup().await; setup().await;
// make stake account // make stake account
let user = Keypair::new();
let user_stake = Keypair::new(); let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default(); let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized { let authorized = stake_program::Authorized {
staker: stake_pool_accounts.deposit_authority, staker: user.pubkey(),
withdrawer: stake_pool_accounts.deposit_authority, withdrawer: user.pubkey(),
}; };
create_independent_stake_account( create_independent_stake_account(
&mut banks_client, &mut banks_client,
@ -784,12 +571,13 @@ async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() {
&user_stake, &user_stake,
&authorized, &authorized,
&lockup, &lockup,
TEST_STAKE_AMOUNT,
) )
.await; .await;
let outside_mint = Keypair::new(); let outside_mint = Keypair::new();
let outside_withdraw_auth = Keypair::new(); let outside_withdraw_auth = Keypair::new();
let outside_owner = Keypair::new(); let outside_manager = Keypair::new();
let outside_pool_fee_acc = Keypair::new(); let outside_pool_fee_acc = Keypair::new();
create_mint( create_mint(
@ -808,7 +596,7 @@ async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() {
&recent_blockhash, &recent_blockhash,
&outside_pool_fee_acc, &outside_pool_fee_acc,
&outside_mint.pubkey(), &outside_mint.pubkey(),
&outside_owner.pubkey(), &outside_manager.pubkey(),
) )
.await .await
.unwrap(); .unwrap();
@ -821,6 +609,7 @@ async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() {
&user_stake.pubkey(), &user_stake.pubkey(),
&outside_pool_fee_acc.pubkey(), &outside_pool_fee_acc.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&user,
) )
.await .await
.err() .err()
@ -843,3 +632,180 @@ async fn test_deposit_with_uninitialized_validator_list() {} // TODO
#[tokio::test] #[tokio::test]
async fn test_deposit_with_out_of_dated_pool_balances() {} // TODO async fn test_deposit_with_out_of_dated_pool_balances() {} // TODO
#[tokio::test]
async fn success_with_deposit_authority() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let deposit_authority = Keypair::new();
let stake_pool_accounts = StakePoolAccounts::new_with_deposit_authority(deposit_authority);
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let user = Keypair::new();
let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized {
staker: user.pubkey(),
withdrawer: user.pubkey(),
};
let _stake_lamports = create_independent_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake,
&authorized,
&lockup,
TEST_STAKE_AMOUNT,
)
.await;
create_vote(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake_account.validator,
&validator_stake_account.vote,
)
.await;
delegate_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&user,
&validator_stake_account.vote.pubkey(),
)
.await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
stake_pool_accounts
.deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&user_pool_account.pubkey(),
&validator_stake_account.stake_account,
&user,
)
.await
.unwrap();
}
#[tokio::test]
async fn fail_without_deposit_authority_signature() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let deposit_authority = Keypair::new();
let mut stake_pool_accounts = StakePoolAccounts::new_with_deposit_authority(deposit_authority);
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let user = Keypair::new();
let user_stake = Keypair::new();
let lockup = stake_program::Lockup::default();
let authorized = stake_program::Authorized {
staker: user.pubkey(),
withdrawer: user.pubkey(),
};
let _stake_lamports = create_independent_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake,
&authorized,
&lockup,
TEST_STAKE_AMOUNT,
)
.await;
create_vote(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake_account.validator,
&validator_stake_account.vote,
)
.await;
delegate_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&user,
&validator_stake_account.vote.pubkey(),
)
.await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
let wrong_depositor = Keypair::new();
stake_pool_accounts.deposit_authority = wrong_depositor.pubkey();
stake_pool_accounts.deposit_authority_keypair = Some(wrong_depositor);
let error = stake_pool_accounts
.deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.pubkey(),
&user_pool_account.pubkey(),
&validator_stake_account.stake_account,
&user,
)
.await
.unwrap_err()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
assert_eq!(
error_index,
error::StakePoolError::InvalidProgramAddress as u32
);
}
_ => panic!("Wrong error occurs while try to make a deposit with wrong stake program ID"),
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,384 @@
#![cfg(feature = "test-bpf")]
mod helpers;
use {
bincode::deserialize,
helpers::*,
solana_program::{
clock::Epoch, hash::Hash, instruction::InstructionError, pubkey::Pubkey,
system_instruction::SystemError,
},
solana_program_test::*,
solana_sdk::{
signature::{Keypair, Signer},
transaction::{Transaction, TransactionError},
},
spl_stake_pool::{error::StakePoolError, id, instruction, stake_program},
};
async fn setup() -> (
BanksClient,
Keypair,
Hash,
StakePoolAccounts,
ValidatorStakeAccount,
u64,
) {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
let reserve_lamports = 100_000_000_000;
stake_pool_accounts
.initialize_stake_pool(
&mut banks_client,
&payer,
&recent_blockhash,
reserve_lamports,
)
.await
.unwrap();
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let _deposit_info = simple_deposit(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&validator_stake_account,
5_000_000,
)
.await
.unwrap();
(
banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake_account,
reserve_lamports,
)
}
#[tokio::test]
async fn success() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
reserve_lamports,
) = setup().await;
// Save reserve stake
let pre_reserve_stake_account = get_account(
&mut banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
)
.await;
// Check no transient stake
let transient_account = banks_client
.get_account(validator_stake.transient_stake_account)
.await
.unwrap();
assert!(transient_account.is_none());
let rent = banks_client.get_rent().await.unwrap();
let lamports = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
let reserve_lamports = reserve_lamports - lamports;
let error = stake_pool_accounts
.increase_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.transient_stake_account,
&validator_stake.vote.pubkey(),
reserve_lamports,
)
.await;
assert!(error.is_none());
// Check reserve stake account balance
let reserve_stake_account = get_account(
&mut banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
)
.await;
let reserve_stake_state =
deserialize::<stake_program::StakeState>(&reserve_stake_account.data).unwrap();
assert_eq!(
pre_reserve_stake_account.lamports - reserve_lamports,
reserve_stake_account.lamports
);
assert!(reserve_stake_state.delegation().is_none());
// Check transient stake account state and balance
let transient_stake_account =
get_account(&mut banks_client, &validator_stake.transient_stake_account).await;
let transient_stake_state =
deserialize::<stake_program::StakeState>(&transient_stake_account.data).unwrap();
assert_eq!(transient_stake_account.lamports, reserve_lamports);
assert_ne!(
transient_stake_state.delegation().unwrap().activation_epoch,
Epoch::MAX
);
}
#[tokio::test]
async fn fail_with_wrong_withdraw_authority() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
reserve_lamports,
) = setup().await;
let wrong_authority = Pubkey::new_unique();
let transaction = Transaction::new_signed_with_payer(
&[instruction::increase_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&wrong_authority,
&stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&validator_stake.transient_stake_account,
&validator_stake.vote.pubkey(),
reserve_lamports / 2,
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.staker],
recent_blockhash,
);
let error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakePoolError::InvalidProgramAddress as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error"),
}
}
#[tokio::test]
async fn fail_with_wrong_validator_list() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
reserve_lamports,
) = setup().await;
let wrong_validator_list = Pubkey::new_unique();
let transaction = Transaction::new_signed_with_payer(
&[instruction::increase_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.withdraw_authority,
&wrong_validator_list,
&stake_pool_accounts.reserve_stake.pubkey(),
&validator_stake.transient_stake_account,
&validator_stake.vote.pubkey(),
reserve_lamports / 2,
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.staker],
recent_blockhash,
);
let error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error"),
}
}
#[tokio::test]
async fn fail_with_unknown_validator() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
_validator_stake,
reserve_lamports,
) = setup().await;
let unknown_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
unknown_stake
.create_and_delegate(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.staker,
)
.await;
let transaction = Transaction::new_signed_with_payer(
&[instruction::increase_validator_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.withdraw_authority,
&stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&unknown_stake.transient_stake_account,
&unknown_stake.vote.pubkey(),
reserve_lamports / 2,
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.staker],
recent_blockhash,
);
let error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = StakePoolError::ValidatorNotFound as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error"),
}
}
#[tokio::test]
async fn fail_increase_twice() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
reserve_lamports,
) = setup().await;
let error = stake_pool_accounts
.increase_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.transient_stake_account,
&validator_stake.vote.pubkey(),
reserve_lamports / 3,
)
.await;
assert!(error.is_none());
let error = stake_pool_accounts
.increase_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.transient_stake_account,
&validator_stake.vote.pubkey(),
reserve_lamports / 4,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = SystemError::AccountAlreadyInUse as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error"),
}
}
#[tokio::test]
async fn fail_with_small_lamport_amount() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
_reserve_lamports,
) = setup().await;
let rent = banks_client.get_rent().await.unwrap();
let lamports = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
let error = stake_pool_accounts
.increase_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.transient_stake_account,
&validator_stake.vote.pubkey(),
lamports,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::InvalidError) => {}
_ => panic!("Wrong error"),
}
}
#[tokio::test]
async fn fail_overdraw_reserve() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
reserve_lamports,
) = setup().await;
let error = stake_pool_accounts
.increase_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.transient_stake_account,
&validator_stake.vote.pubkey(),
reserve_lamports,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::InsufficientFunds) => {}
_ => panic!("Wrong error occurs while overdrawing reserve stake"),
}
}

View File

@ -3,13 +3,14 @@
mod helpers; mod helpers;
use { use {
borsh::BorshSerialize, borsh::{BorshDeserialize, BorshSerialize},
helpers::*, helpers::*,
solana_program::{ solana_program::{
borsh::get_packed_len, borsh::get_packed_len,
hash::Hash, hash::Hash,
instruction::{AccountMeta, Instruction}, instruction::{AccountMeta, Instruction},
program_pack::Pack, program_pack::Pack,
pubkey::Pubkey,
system_instruction, sysvar, system_instruction, sysvar,
}, },
solana_program_test::*, solana_program_test::*,
@ -19,11 +20,11 @@ use {
}, },
spl_stake_pool::{ spl_stake_pool::{
borsh::{get_instance_packed_len, try_from_slice_unchecked}, borsh::{get_instance_packed_len, try_from_slice_unchecked},
error, id, instruction, state, error, id, instruction, stake_program, state,
}, },
}; };
async fn create_mint_and_token_account( async fn create_required_accounts(
banks_client: &mut BanksClient, banks_client: &mut BanksClient,
payer: &Keypair, payer: &Keypair,
recent_blockhash: &Hash, recent_blockhash: &Hash,
@ -45,18 +46,32 @@ async fn create_mint_and_token_account(
recent_blockhash, recent_blockhash,
&stake_pool_accounts.pool_fee_account, &stake_pool_accounts.pool_fee_account,
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.manager.pubkey(),
) )
.await .await
.unwrap(); .unwrap();
create_independent_stake_account(
banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.reserve_stake,
&stake_program::Authorized {
staker: stake_pool_accounts.withdraw_authority,
withdrawer: stake_pool_accounts.withdraw_authority,
},
&stake_program::Lockup::default(),
1,
)
.await;
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_initialize() { async fn success() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
@ -77,11 +92,11 @@ async fn test_stake_pool_initialize() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_already_initialized_stake_pool() { async fn fail_double_initialize() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
@ -91,7 +106,7 @@ async fn test_initialize_already_initialized_stake_pool() {
second_stake_pool_accounts.stake_pool = stake_pool_accounts.stake_pool; second_stake_pool_accounts.stake_pool = stake_pool_accounts.stake_pool;
let transaction_error = second_stake_pool_accounts let transaction_error = second_stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &latest_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &latest_blockhash, 1)
.await .await
.err() .err()
.unwrap(); .unwrap();
@ -108,11 +123,11 @@ async fn test_initialize_already_initialized_stake_pool() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_already_initialized_stake_list_storage() { async fn fail_with_already_initialized_validator_list() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
@ -122,7 +137,7 @@ async fn test_initialize_stake_pool_with_already_initialized_stake_list_storage(
second_stake_pool_accounts.validator_list = stake_pool_accounts.validator_list; second_stake_pool_accounts.validator_list = stake_pool_accounts.validator_list;
let transaction_error = second_stake_pool_accounts let transaction_error = second_stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &latest_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &latest_blockhash, 1)
.await .await
.err() .err()
.unwrap(); .unwrap();
@ -139,16 +154,16 @@ async fn test_initialize_stake_pool_with_already_initialized_stake_list_storage(
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_high_fee() { async fn fail_with_high_fee() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let mut stake_pool_accounts = StakePoolAccounts::new(); let mut stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts.fee = instruction::Fee { stake_pool_accounts.fee = state::Fee {
numerator: 100001, numerator: 100001,
denominator: 100000, denominator: 100000,
}; };
let transaction_error = stake_pool_accounts let transaction_error = stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.err() .err()
.unwrap(); .unwrap();
@ -165,11 +180,11 @@ async fn test_initialize_stake_pool_with_high_fee() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_wrong_max_validators() { async fn fail_with_wrong_max_validators() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
create_mint_and_token_account( create_required_accounts(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
@ -204,15 +219,17 @@ async fn test_initialize_stake_pool_with_wrong_max_validators() {
instruction::initialize( instruction::initialize(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.manager.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.validator_list.pubkey(), &stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(), &stake_pool_accounts.pool_fee_account.pubkey(),
&spl_token::id(), &spl_token::id(),
stake_pool_accounts.fee.clone(), None,
stake_pool_accounts.fee,
stake_pool_accounts.max_validators, stake_pool_accounts.max_validators,
) ),
.unwrap(),
], ],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
@ -221,7 +238,7 @@ async fn test_initialize_stake_pool_with_wrong_max_validators() {
&payer, &payer,
&stake_pool_accounts.stake_pool, &stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list, &stake_pool_accounts.validator_list,
&stake_pool_accounts.owner, &stake_pool_accounts.manager,
], ],
recent_blockhash, recent_blockhash,
); );
@ -244,12 +261,12 @@ async fn test_initialize_stake_pool_with_wrong_max_validators() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_wrong_mint_authority() { async fn fail_with_wrong_mint_authority() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
let wrong_mint = Keypair::new(); let wrong_mint = Keypair::new();
create_mint_and_token_account( create_required_accounts(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
@ -274,9 +291,12 @@ async fn test_initialize_stake_pool_with_wrong_mint_authority() {
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.stake_pool, &stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list, &stake_pool_accounts.validator_list,
&stake_pool_accounts.reserve_stake.pubkey(),
&wrong_mint.pubkey(), &wrong_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(), &stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.owner, &stake_pool_accounts.manager,
&stake_pool_accounts.staker.pubkey(),
&None,
&stake_pool_accounts.fee, &stake_pool_accounts.fee,
stake_pool_accounts.max_validators, stake_pool_accounts.max_validators,
) )
@ -297,7 +317,7 @@ async fn test_initialize_stake_pool_with_wrong_mint_authority() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_wrong_token_program_id() { async fn fail_with_wrong_token_program_id() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
@ -358,15 +378,17 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
instruction::initialize( instruction::initialize(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.manager.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.validator_list.pubkey(), &stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(), &stake_pool_accounts.pool_fee_account.pubkey(),
&wrong_token_program.pubkey(), &wrong_token_program.pubkey(),
stake_pool_accounts.fee.clone(), None,
stake_pool_accounts.fee,
stake_pool_accounts.max_validators, stake_pool_accounts.max_validators,
) ),
.unwrap(),
], ],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
@ -375,7 +397,7 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
&payer, &payer,
&stake_pool_accounts.stake_pool, &stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list, &stake_pool_accounts.validator_list,
&stake_pool_accounts.owner, &stake_pool_accounts.manager,
], ],
recent_blockhash, recent_blockhash,
); );
@ -396,7 +418,7 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_wrong_fee_accounts_owner() { async fn fail_with_wrong_fee_account() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
@ -434,39 +456,35 @@ async fn test_initialize_stake_pool_with_wrong_fee_accounts_owner() {
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.stake_pool, &stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list, &stake_pool_accounts.validator_list,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(), &stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.owner, &stake_pool_accounts.manager,
&stake_pool_accounts.staker.pubkey(),
&None,
&stake_pool_accounts.fee, &stake_pool_accounts.fee,
stake_pool_accounts.max_validators, stake_pool_accounts.max_validators,
) )
.await .await
.err() .err()
.unwrap()
.unwrap(); .unwrap();
match transaction_error { assert_eq!(
TransportError::TransactionError(TransactionError::InstructionError( transaction_error,
_, TransactionError::InstructionError(2, InstructionError::IncorrectProgramId)
InstructionError::Custom(error_index), );
)) => {
let program_error = error::StakePoolError::InvalidFeeAccount as u32;
assert_eq!(error_index, program_error);
}
_ => panic!(
"Wrong error occurs while try to initialize stake pool with wrong fee account's owner"
),
}
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_wrong_withdraw_authority() { async fn fail_with_wrong_withdraw_authority() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let mut stake_pool_accounts = StakePoolAccounts::new(); let mut stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts.withdraw_authority = Keypair::new().pubkey(); stake_pool_accounts.withdraw_authority = Keypair::new().pubkey();
let transaction_error = stake_pool_accounts let transaction_error = stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.err() .err()
.unwrap(); .unwrap();
@ -486,11 +504,11 @@ async fn test_initialize_stake_pool_with_wrong_withdraw_authority() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_not_rent_exempt_pool() { async fn fail_with_not_rent_exempt_pool() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
create_mint_and_token_account( create_required_accounts(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
@ -524,15 +542,17 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
instruction::initialize( instruction::initialize(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.manager.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.validator_list.pubkey(), &stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(), &stake_pool_accounts.pool_fee_account.pubkey(),
&spl_token::id(), &spl_token::id(),
stake_pool_accounts.fee.clone(), None,
stake_pool_accounts.fee,
stake_pool_accounts.max_validators, stake_pool_accounts.max_validators,
) ),
.unwrap(),
], ],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
@ -541,7 +561,7 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
&payer, &payer,
&stake_pool_accounts.stake_pool, &stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list, &stake_pool_accounts.validator_list,
&stake_pool_accounts.owner, &stake_pool_accounts.manager,
], ],
recent_blockhash, recent_blockhash,
); );
@ -558,11 +578,11 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_with_not_rent_exempt_validator_list() { async fn fail_with_not_rent_exempt_validator_list() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
create_mint_and_token_account( create_required_accounts(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
@ -596,15 +616,17 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_list() {
instruction::initialize( instruction::initialize(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.manager.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.validator_list.pubkey(), &stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(), &stake_pool_accounts.pool_fee_account.pubkey(),
&spl_token::id(), &spl_token::id(),
stake_pool_accounts.fee.clone(), None,
stake_pool_accounts.fee,
stake_pool_accounts.max_validators, stake_pool_accounts.max_validators,
) ),
.unwrap(),
], ],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
@ -613,7 +635,7 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_list() {
&payer, &payer,
&stake_pool_accounts.stake_pool, &stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list, &stake_pool_accounts.validator_list,
&stake_pool_accounts.owner, &stake_pool_accounts.manager,
], ],
recent_blockhash, recent_blockhash,
); );
@ -632,11 +654,11 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_list() {
} }
#[tokio::test] #[tokio::test]
async fn test_initialize_stake_pool_without_owner_signature() { async fn fail_without_manager_signature() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
create_mint_and_token_account( create_required_accounts(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
@ -653,14 +675,16 @@ async fn test_initialize_stake_pool_without_owner_signature() {
let rent_validator_list = rent.minimum_balance(validator_list_size); let rent_validator_list = rent.minimum_balance(validator_list_size);
let init_data = instruction::StakePoolInstruction::Initialize { let init_data = instruction::StakePoolInstruction::Initialize {
fee: stake_pool_accounts.fee.clone(), fee: stake_pool_accounts.fee,
max_validators: stake_pool_accounts.max_validators, max_validators: stake_pool_accounts.max_validators,
}; };
let data = init_data.try_to_vec().unwrap(); let data = init_data.try_to_vec().unwrap();
let accounts = vec![ let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), true), AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), true),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false), AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false),
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.reserve_stake.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.pool_mint.pubkey(), false), AccountMeta::new_readonly(stake_pool_accounts.pool_mint.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.pool_fee_account.pubkey(), false), AccountMeta::new_readonly(stake_pool_accounts.pool_fee_account.pubkey(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::clock::id(), false),
@ -716,7 +740,298 @@ async fn test_initialize_stake_pool_without_owner_signature() {
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!( _ => panic!(
"Wrong error occurs while try to initialize stake pool without owner's signature" "Wrong error occurs while try to initialize stake pool without manager's signature"
), ),
} }
} }
#[tokio::test]
async fn fail_with_pre_minted_pool_tokens() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
let mint_authority = Keypair::new();
create_mint(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.pool_mint,
&mint_authority.pubkey(),
)
.await
.unwrap();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.pool_fee_account,
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.manager.pubkey(),
)
.await
.unwrap();
mint_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&mint_authority,
1,
)
.await
.unwrap();
let transaction_error = create_stake_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.manager,
&stake_pool_accounts.staker.pubkey(),
&None,
&stake_pool_accounts.fee,
stake_pool_accounts.max_validators,
)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = error::StakePoolError::NonZeroPoolTokenSupply as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to initialize stake pool with wrong mint authority of pool fee account"),
}
}
#[tokio::test]
async fn fail_with_bad_reserve() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
let wrong_authority = Pubkey::new_unique();
create_required_accounts(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
{
let bad_stake = Keypair::new();
create_independent_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&bad_stake,
&stake_program::Authorized {
staker: wrong_authority,
withdrawer: stake_pool_accounts.withdraw_authority,
},
&stake_program::Lockup::default(),
1,
)
.await;
let error = create_stake_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list,
&bad_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.manager,
&stake_pool_accounts.staker.pubkey(),
&None,
&stake_pool_accounts.fee,
stake_pool_accounts.max_validators,
)
.await
.err()
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
2,
InstructionError::Custom(error::StakePoolError::WrongStakeState as u32),
)
);
}
{
let bad_stake = Keypair::new();
create_independent_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&bad_stake,
&stake_program::Authorized {
staker: stake_pool_accounts.withdraw_authority,
withdrawer: wrong_authority,
},
&stake_program::Lockup::default(),
1,
)
.await;
let error = create_stake_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list,
&bad_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.manager,
&stake_pool_accounts.staker.pubkey(),
&None,
&stake_pool_accounts.fee,
stake_pool_accounts.max_validators,
)
.await
.err()
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
2,
InstructionError::Custom(error::StakePoolError::WrongStakeState as u32),
)
);
}
{
let bad_stake = Keypair::new();
create_independent_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&bad_stake,
&stake_program::Authorized {
staker: stake_pool_accounts.withdraw_authority,
withdrawer: stake_pool_accounts.withdraw_authority,
},
&stake_program::Lockup {
custodian: wrong_authority,
..stake_program::Lockup::default()
},
1,
)
.await;
let error = create_stake_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list,
&bad_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.manager,
&stake_pool_accounts.staker.pubkey(),
&None,
&stake_pool_accounts.fee,
stake_pool_accounts.max_validators,
)
.await
.err()
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
2,
InstructionError::Custom(error::StakePoolError::WrongStakeState as u32),
)
);
}
{
let bad_stake = Keypair::new();
let rent = banks_client.get_rent().await.unwrap();
let lamports = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
let transaction = Transaction::new_signed_with_payer(
&[system_instruction::create_account(
&payer.pubkey(),
&bad_stake.pubkey(),
lamports,
std::mem::size_of::<stake_program::StakeState>() as u64,
&stake_program::id(),
)],
Some(&payer.pubkey()),
&[&payer, &bad_stake],
recent_blockhash,
);
banks_client.process_transaction(transaction).await.unwrap();
let error = create_stake_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.stake_pool,
&stake_pool_accounts.validator_list,
&bad_stake.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.manager,
&stake_pool_accounts.staker.pubkey(),
&None,
&stake_pool_accounts.fee,
stake_pool_accounts.max_validators,
)
.await
.err()
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
2,
InstructionError::Custom(error::StakePoolError::WrongStakeState as u32),
)
);
}
}
#[tokio::test]
async fn success_with_required_deposit_authority() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let deposit_authority = Keypair::new();
let stake_pool_accounts = StakePoolAccounts::new_with_deposit_authority(deposit_authority);
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
// Stake pool now exists
let stake_pool_account =
get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = state::StakePool::try_from_slice(stake_pool_account.data.as_slice()).unwrap();
assert_eq!(
stake_pool.deposit_authority,
stake_pool_accounts.deposit_authority
);
}

View File

@ -0,0 +1,178 @@
#![cfg(feature = "test-bpf")]
mod helpers;
use {
borsh::BorshDeserialize,
helpers::*,
solana_program::hash::Hash,
solana_program_test::*,
solana_sdk::{
instruction::InstructionError, signature::Keypair, signature::Signer,
transaction::Transaction, transaction::TransactionError,
},
spl_stake_pool::{
error, id, instruction,
state::{Fee, StakePool},
},
};
async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, Fee) {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let new_fee = Fee {
numerator: 10,
denominator: 10,
};
(
banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
new_fee,
)
}
#[tokio::test]
async fn success() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_fee) = setup().await;
let transaction = Transaction::new_signed_with_payer(
&[instruction::set_fee(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.manager.pubkey(),
new_fee,
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.manager],
recent_blockhash,
);
banks_client.process_transaction(transaction).await.unwrap();
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(stake_pool.fee, new_fee);
}
#[tokio::test]
async fn fail_wrong_manager() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_fee) = setup().await;
let wrong_manager = Keypair::new();
let transaction = Transaction::new_signed_with_payer(
&[instruction::set_fee(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&wrong_manager.pubkey(),
new_fee,
)],
Some(&payer.pubkey()),
&[&payer, &wrong_manager],
recent_blockhash,
);
let error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = error::StakePoolError::WrongManager as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while malicious try to set manager"),
}
}
#[tokio::test]
async fn fail_bad_fee() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, _new_fee) = setup().await;
let new_fee = Fee {
numerator: 11,
denominator: 10,
};
let transaction = Transaction::new_signed_with_payer(
&[instruction::set_fee(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.manager.pubkey(),
new_fee,
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.manager],
recent_blockhash,
);
let error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = error::StakePoolError::FeeTooHigh as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while malicious try to set manager"),
}
}
#[tokio::test]
async fn fail_not_updated() {
let mut context = program_test().start_with_context().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
1,
)
.await
.unwrap();
let new_fee = Fee {
numerator: 10,
denominator: 100,
};
// move forward so an update is required
context.warp_to_slot(50_000).unwrap();
let transaction = Transaction::new_signed_with_payer(
&[instruction::set_fee(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.manager.pubkey(),
new_fee,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &stake_pool_accounts.manager],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
let program_error = error::StakePoolError::StakeListAndPoolOutOfDate as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while malicious try to set manager"),
}
}

View File

@ -28,19 +28,19 @@ async fn setup() -> (
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let new_pool_fee = Keypair::new(); let new_pool_fee = Keypair::new();
let new_owner = Keypair::new(); let new_manager = Keypair::new();
create_token_account( create_token_account(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&new_pool_fee, &new_pool_fee,
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&new_owner.pubkey(), &new_manager.pubkey(),
) )
.await .await
.unwrap(); .unwrap();
@ -51,52 +51,50 @@ async fn setup() -> (
recent_blockhash, recent_blockhash,
stake_pool_accounts, stake_pool_accounts,
new_pool_fee, new_pool_fee,
new_owner, new_manager,
) )
} }
#[tokio::test] #[tokio::test]
async fn test_set_owner() { async fn test_set_manager() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_owner) = let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) =
setup().await; setup().await;
let mut transaction = Transaction::new_with_payer( let mut transaction = Transaction::new_with_payer(
&[instruction::set_owner( &[instruction::set_manager(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.manager.pubkey(),
&new_owner.pubkey(), &new_manager.pubkey(),
&new_pool_fee.pubkey(), &new_pool_fee.pubkey(),
) )],
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap(); banks_client.process_transaction(transaction).await.unwrap();
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap(); let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(stake_pool.owner, new_owner.pubkey()); assert_eq!(stake_pool.manager, new_manager.pubkey());
} }
#[tokio::test] #[tokio::test]
async fn test_set_owner_by_malicious() { async fn test_set_manager_by_malicious() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_owner) = let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) =
setup().await; setup().await;
let mut transaction = Transaction::new_with_payer( let mut transaction = Transaction::new_with_payer(
&[instruction::set_owner( &[instruction::set_manager(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&new_owner.pubkey(), &new_manager.pubkey(),
&new_owner.pubkey(), &new_manager.pubkey(),
&new_pool_fee.pubkey(), &new_pool_fee.pubkey(),
) )],
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &new_owner], recent_blockhash); transaction.sign(&[&payer, &new_manager], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -108,25 +106,25 @@ async fn test_set_owner_by_malicious() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::WrongOwner as u32; let program_error = error::StakePoolError::WrongManager as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while malicious try to set owner"), _ => panic!("Wrong error occurs while malicious try to set manager"),
} }
} }
#[tokio::test] #[tokio::test]
async fn test_set_owner_without_signature() { async fn test_set_manager_without_signature() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_owner) = let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) =
setup().await; setup().await;
let data = instruction::StakePoolInstruction::SetOwner let data = instruction::StakePoolInstruction::SetManager
.try_to_vec() .try_to_vec()
.unwrap(); .unwrap();
let accounts = vec![ let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false), AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false),
AccountMeta::new_readonly(new_owner.pubkey(), false), AccountMeta::new_readonly(new_manager.pubkey(), false),
AccountMeta::new_readonly(new_pool_fee.pubkey(), false), AccountMeta::new_readonly(new_pool_fee.pubkey(), false),
]; ];
let instruction = Instruction { let instruction = Instruction {
@ -151,23 +149,23 @@ async fn test_set_owner_without_signature() {
let program_error = error::StakePoolError::SignatureMissing as u32; let program_error = error::StakePoolError::SignatureMissing as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while try to set new owner without signature"), _ => panic!("Wrong error occurs while try to set new manager without signature"),
} }
} }
#[tokio::test] #[tokio::test]
async fn test_set_owner_with_wrong_mint_for_pool_fee_acc() { async fn test_set_manager_with_wrong_mint_for_pool_fee_acc() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let new_mint = Keypair::new(); let new_mint = Keypair::new();
let new_withdraw_auth = Keypair::new(); let new_withdraw_auth = Keypair::new();
let new_pool_fee = Keypair::new(); let new_pool_fee = Keypair::new();
let new_owner = Keypair::new(); let new_manager = Keypair::new();
create_mint( create_mint(
&mut banks_client, &mut banks_client,
@ -184,23 +182,22 @@ async fn test_set_owner_with_wrong_mint_for_pool_fee_acc() {
&recent_blockhash, &recent_blockhash,
&new_pool_fee, &new_pool_fee,
&new_mint.pubkey(), &new_mint.pubkey(),
&new_owner.pubkey(), &new_manager.pubkey(),
) )
.await .await
.unwrap(); .unwrap();
let mut transaction = Transaction::new_with_payer( let mut transaction = Transaction::new_with_payer(
&[instruction::set_owner( &[instruction::set_manager(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.manager.pubkey(),
&new_owner.pubkey(), &new_manager.pubkey(),
&new_pool_fee.pubkey(), &new_pool_fee.pubkey(),
) )],
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -215,6 +212,6 @@ async fn test_set_owner_with_wrong_mint_for_pool_fee_acc() {
let program_error = error::StakePoolError::WrongAccountMint as u32; let program_error = error::StakePoolError::WrongAccountMint as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while try to set new owner with wrong mint"), _ => panic!("Wrong error occurs while try to set new manager with wrong mint"),
} }
} }

View File

@ -0,0 +1,172 @@
#![cfg(feature = "test-bpf")]
mod helpers;
use {
borsh::{BorshDeserialize, BorshSerialize},
helpers::*,
solana_program::{
hash::Hash,
instruction::{AccountMeta, Instruction},
},
solana_program_test::*,
solana_sdk::{
instruction::InstructionError, signature::Keypair, signature::Signer,
transaction::Transaction, transaction::TransactionError, transport::TransportError,
},
spl_stake_pool::{error, id, instruction, state},
};
async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, Keypair) {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let new_staker = Keypair::new();
(
banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
new_staker,
)
}
#[tokio::test]
async fn success_set_staker_as_manager() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) =
setup().await;
let mut transaction = Transaction::new_with_payer(
&[instruction::set_staker(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.manager.pubkey(),
&new_staker.pubkey(),
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(stake_pool.staker, new_staker.pubkey());
}
#[tokio::test]
async fn success_set_staker_as_staker() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) =
setup().await;
let mut transaction = Transaction::new_with_payer(
&[instruction::set_staker(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.staker.pubkey(),
&new_staker.pubkey(),
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(stake_pool.staker, new_staker.pubkey());
let mut transaction = Transaction::new_with_payer(
&[instruction::set_staker(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&new_staker.pubkey(),
&stake_pool_accounts.staker.pubkey(),
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &new_staker], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(stake_pool.staker, stake_pool_accounts.staker.pubkey());
}
#[tokio::test]
async fn fail_wrong_manager() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) =
setup().await;
let mut transaction = Transaction::new_with_payer(
&[instruction::set_staker(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&new_staker.pubkey(),
&new_staker.pubkey(),
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &new_staker], recent_blockhash);
let transaction_error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = error::StakePoolError::SignatureMissing as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while malicious try to set manager"),
}
}
#[tokio::test]
async fn fail_set_staker_without_signature() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) =
setup().await;
let data = instruction::StakePoolInstruction::SetStaker
.try_to_vec()
.unwrap();
let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false),
AccountMeta::new_readonly(new_staker.pubkey(), false),
];
let instruction = Instruction {
program_id: id(),
accounts,
data,
};
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer], recent_blockhash);
let transaction_error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = error::StakePoolError::SignatureMissing as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to set new manager without signature"),
}
}

View File

@ -3,60 +3,219 @@
mod helpers; mod helpers;
use { use {
borsh::BorshDeserialize,
helpers::*, helpers::*,
solana_program::{instruction::InstructionError, pubkey::Pubkey},
solana_program_test::*, solana_program_test::*,
solana_sdk::{ solana_sdk::{
instruction::InstructionError, signature::Keypair, signature::Signer, signature::{Keypair, Signer},
transaction::Transaction, transaction::TransactionError, transport::TransportError, transaction::TransactionError,
}, },
spl_stake_pool::*, spl_stake_pool::{error::StakePoolError, state::StakePool},
}; };
#[tokio::test] async fn setup() -> (
async fn test_update_stake_pool_balance() { ProgramTestContext,
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; StakePoolAccounts,
Vec<ValidatorStakeAccount>,
) {
let mut context = program_test().start_with_context().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
1,
)
.await .await
.unwrap(); .unwrap();
// TODO: Waiting for the ability to advance clock (or modify account data) to finish the tests // Add several accounts
let mut stake_accounts: Vec<ValidatorStakeAccount> = vec![];
const STAKE_ACCOUNTS: u64 = 3;
for _ in 0..STAKE_ACCOUNTS {
let validator_stake_account = simple_add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
)
.await;
let _deposit_info = simple_deposit(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
&validator_stake_account,
TEST_STAKE_AMOUNT,
)
.await
.unwrap();
stake_accounts.push(validator_stake_account);
}
(context, stake_pool_accounts, stake_accounts)
} }
#[tokio::test] #[tokio::test]
async fn test_update_stake_pool_balance_with_wrong_validator_list() { async fn success() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut context, stake_pool_accounts, stake_accounts) = setup().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
.await
.unwrap();
let wrong_stake_list_storage = Keypair::new(); let pre_balance = get_validator_list_sum(
let mut transaction = Transaction::new_with_payer( &mut context.banks_client,
&[instruction::update_stake_pool_balance( &stake_pool_accounts.reserve_stake.pubkey(),
&id(), &stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.stake_pool.pubkey(), )
&wrong_stake_list_storage.pubkey(), .await;
let stake_pool = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(pre_balance, stake_pool.total_stake_lamports);
let pre_token_supply = get_token_supply(
&mut context.banks_client,
&stake_pool_accounts.pool_mint.pubkey(),
)
.await;
let error = stake_pool_accounts
.update_stake_pool_balance(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
) )
.unwrap()], .await;
Some(&payer.pubkey()), assert!(error.is_none());
);
transaction.sign(&[&payer], recent_blockhash); // Add extra funds, simulating rewards
let transaction_error = banks_client const EXTRA_STAKE_AMOUNT: u64 = 1_000_000;
.process_transaction(transaction) for stake_account in &stake_accounts {
transfer(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_account.stake_account,
EXTRA_STAKE_AMOUNT,
)
.await;
}
// Update epoch
context.warp_to_slot(50_000).unwrap();
// Update list and pool
let error = stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
)
.await;
assert!(error.is_none());
// Check fee
let post_balance = get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let stake_pool = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(post_balance, stake_pool.total_stake_lamports);
let actual_fee = get_token_balance(
&mut context.banks_client,
&stake_pool_accounts.pool_fee_account.pubkey(),
)
.await;
let pool_token_supply = get_token_supply(
&mut context.banks_client,
&stake_pool_accounts.pool_mint.pubkey(),
)
.await;
let stake_pool_info = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
let expected_fee = (post_balance - pre_balance) * pre_token_supply / pre_balance
* stake_pool.fee.numerator
/ stake_pool.fee.denominator;
assert_eq!(actual_fee, expected_fee);
assert_eq!(pool_token_supply, stake_pool.pool_token_supply);
}
#[tokio::test]
async fn fail_with_wrong_validator_list() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let mut stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.err()
.unwrap(); .unwrap();
match transaction_error { let wrong_validator_list = Keypair::new();
TransportError::TransactionError(TransactionError::InstructionError( stake_pool_accounts.validator_list = wrong_validator_list;
let error = stake_pool_accounts
.update_stake_pool_balance(&mut banks_client, &payer, &recent_blockhash)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { ) => {
let program_error = error::StakePoolError::InvalidValidatorStakeList as u32; let program_error = StakePoolError::InvalidValidatorStakeList as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to update pool balance with wrong validator stake list account"),
}
}
#[tokio::test]
async fn fail_with_wrong_pool_fee_account() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let mut stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let wrong_fee_account = Keypair::new();
stake_pool_accounts.pool_fee_account = wrong_fee_account;
let error = stake_pool_accounts
.update_stake_pool_balance(&mut banks_client, &payer, &recent_blockhash)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
) => {
let program_error = StakePoolError::InvalidFeeAccount as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while try to update pool balance with wrong validator stake list account"), _ => panic!("Wrong error occurs while try to update pool balance with wrong validator stake list account"),

View File

@ -3,87 +3,607 @@
mod helpers; mod helpers;
use { use {
crate::helpers::TEST_STAKE_AMOUNT, borsh::BorshDeserialize,
helpers::*, helpers::*,
solana_program::{native_token, pubkey::Pubkey}, solana_program::pubkey::Pubkey,
solana_program_test::*, solana_program_test::*,
solana_sdk::signature::Signer, solana_sdk::signature::{Keypair, Signer},
spl_stake_pool::{borsh::try_from_slice_unchecked, stake_program, state}, spl_stake_pool::{
borsh::try_from_slice_unchecked,
stake_program,
state::{StakePool, StakeStatus, ValidatorList},
MAX_VALIDATORS_TO_UPDATE, MINIMUM_ACTIVE_STAKE,
},
}; };
async fn get_list_sum(banks_client: &mut BanksClient, validator_list_key: &Pubkey) -> u64 { async fn setup(
let validator_list = banks_client num_validators: usize,
.get_account(*validator_list_key) ) -> (
.await ProgramTestContext,
.expect("get_account") StakePoolAccounts,
.expect("validator stake list not none"); Vec<ValidatorStakeAccount>,
let validator_list = u64,
try_from_slice_unchecked::<state::ValidatorList>(validator_list.data.as_slice()).unwrap(); u64,
u64,
) {
let mut context = program_test().start_with_context().await;
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
let mut slot = first_normal_slot;
context.warp_to_slot(slot).unwrap();
validator_list let reserve_stake_amount = TEST_STAKE_AMOUNT * num_validators as u64;
.validators
.iter()
.map(|info| info.balance)
.sum()
}
#[tokio::test]
async fn test_update_validator_list_balance() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
reserve_stake_amount + 1,
)
.await .await
.unwrap(); .unwrap();
// Add several accounts // so warmups / cooldowns go faster
let mut stake_accounts: Vec<ValidatorStakeAccount> = vec![]; let validator = Keypair::new();
const STAKE_ACCOUNTS: u64 = 3; let vote = Keypair::new();
for _ in 0..STAKE_ACCOUNTS { create_vote(
stake_accounts.push( &mut context.banks_client,
simple_add_validator_to_pool( &context.payer,
&mut banks_client, &context.last_blockhash,
&payer, &validator,
&recent_blockhash, &vote,
&stake_pool_accounts, )
) .await;
.await, let deposit_account =
); DepositStakeAccount::new_with_vote(vote.pubkey(), validator.pubkey(), 100_000_000_000);
} deposit_account
.create_and_delegate(
// Add stake extra funds &mut context.banks_client,
const EXTRA_STAKE: u64 = 1_000_000; &context.payer,
&context.last_blockhash,
for stake_account in stake_accounts {
transfer(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_account.stake_account,
EXTRA_STAKE,
) )
.await; .await;
// Add several accounts with some stake
let mut stake_accounts: Vec<ValidatorStakeAccount> = vec![];
let mut deposit_accounts: Vec<DepositStakeAccount> = vec![];
for _ in 0..num_validators {
let stake_account = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
stake_account
.create_and_delegate(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts.staker,
)
.await;
let deposit_account = DepositStakeAccount::new_with_vote(
stake_account.vote.pubkey(),
stake_account.stake_account,
TEST_STAKE_AMOUNT,
);
deposit_account
.create_and_delegate(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
)
.await;
stake_accounts.push(stake_account);
deposit_accounts.push(deposit_account);
} }
let rent = banks_client.get_rent().await.unwrap(); // Warp forward so the stakes properly activate, and deposit
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>()) slot += 2 * slots_per_epoch;
+ native_token::sol_to_lamports(1.0); context.warp_to_slot(slot).unwrap();
// Check current balance in the list stake_pool_accounts
assert_eq!( .update_all(
get_list_sum( &mut context.banks_client,
&mut banks_client, &context.payer,
&stake_pool_accounts.validator_list.pubkey() &context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
) )
.await, .await;
STAKE_ACCOUNTS * (stake_rent + TEST_STAKE_AMOUNT)
);
// TODO: Execute update list with updated clock for stake_account in &stake_accounts {
let error = stake_pool_accounts
.add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_account.stake_account,
)
.await;
assert!(error.is_none());
}
for deposit_account in &deposit_accounts {
deposit_account
.deposit(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
)
.await;
}
slot += slots_per_epoch;
context.warp_to_slot(slot).unwrap();
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
)
.await;
(
context,
stake_pool_accounts,
stake_accounts,
TEST_STAKE_AMOUNT,
reserve_stake_amount,
slot,
)
} }
#[tokio::test] #[tokio::test]
async fn test_update_validator_list_balance_with_uninitialized_validator_list() {} // TODO async fn success() {
let num_validators = 5;
let (
mut context,
stake_pool_accounts,
stake_accounts,
validator_lamports,
reserve_lamports,
mut slot,
) = setup(num_validators).await;
// Check current balance in the list
let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
// initially, have all of the deposits plus their rent, and the reserve stake
let initial_lamports =
(validator_lamports + stake_rent) * num_validators as u64 + reserve_lamports;
assert_eq!(
get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey()
)
.await,
initial_lamports,
);
// Simulate rewards
for stake_account in &stake_accounts {
context.increment_vote_account_credits(&stake_account.vote.pubkey(), 100);
}
// Warp one more epoch so the rewards are paid out
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
slot += slots_per_epoch;
context.warp_to_slot(slot).unwrap();
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
)
.await;
let new_lamports = get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
assert!(new_lamports > initial_lamports);
let stake_pool_info = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
assert_eq!(new_lamports, stake_pool.total_stake_lamports);
}
#[tokio::test] #[tokio::test]
async fn test_update_validator_list_balance_with_wrong_stake_state() {} // TODO async fn merge_into_reserve() {
let (mut context, stake_pool_accounts, stake_accounts, lamports, _, mut slot) =
setup(MAX_VALIDATORS_TO_UPDATE).await;
let pre_lamports = get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let reserve_stake = context
.banks_client
.get_account(stake_pool_accounts.reserve_stake.pubkey())
.await
.unwrap()
.unwrap();
let pre_reserve_lamports = reserve_stake.lamports;
// Decrease from all validators
for stake_account in &stake_accounts {
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_account.stake_account,
&stake_account.transient_stake_account,
lamports,
)
.await;
assert!(error.is_none());
}
// Update, should not change, no merges yet
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
)
.await;
let expected_lamports = get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
assert_eq!(pre_lamports, expected_lamports);
let stake_pool_info = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
// Warp one more epoch so the stakes deactivate
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
slot += slots_per_epoch;
context.warp_to_slot(slot).unwrap();
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
)
.await;
let expected_lamports = get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
assert_eq!(pre_lamports, expected_lamports);
let reserve_stake = context
.banks_client
.get_account(stake_pool_accounts.reserve_stake.pubkey())
.await
.unwrap()
.unwrap();
let post_reserve_lamports = reserve_stake.lamports;
assert!(post_reserve_lamports > pre_reserve_lamports);
let stake_pool_info = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
}
#[tokio::test]
async fn merge_into_validator_stake() {
let (mut context, stake_pool_accounts, stake_accounts, lamports, reserve_lamports, mut slot) =
setup(MAX_VALIDATORS_TO_UPDATE).await;
let rent = context.banks_client.get_rent().await.unwrap();
let pre_lamports = get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
// Increase stake to all validators
for stake_account in &stake_accounts {
let error = stake_pool_accounts
.increase_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_account.transient_stake_account,
&stake_account.vote.pubkey(),
reserve_lamports / stake_accounts.len() as u64,
)
.await;
assert!(error.is_none());
}
// Warp just a little bit to get a new blockhash and update again
context.warp_to_slot(slot + 10).unwrap();
// Update, should not change, no merges yet
let error = stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
)
.await;
assert!(error.is_none());
let expected_lamports = get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
assert_eq!(pre_lamports, expected_lamports);
let stake_pool_info = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
let stake_pool_info = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
// Warp one more epoch so the stakes activate, ready to merge
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
slot += slots_per_epoch;
context.warp_to_slot(slot).unwrap();
let error = stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
)
.await;
assert!(error.is_none());
let current_lamports = get_validator_list_sum(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let stake_pool_info = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
assert_eq!(current_lamports, stake_pool.total_stake_lamports);
// Check that transient accounts are gone
for stake_account in &stake_accounts {
assert!(context
.banks_client
.get_account(stake_account.transient_stake_account)
.await
.unwrap()
.is_none());
}
// Check validator stake accounts have the expected balance now:
// validator stake account minimum + deposited lamports + 2 rents + increased lamports
let expected_lamports = MINIMUM_ACTIVE_STAKE
+ lamports
+ reserve_lamports / stake_accounts.len() as u64
+ 2 * rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
for stake_account in &stake_accounts {
let validator_stake =
get_account(&mut context.banks_client, &stake_account.stake_account).await;
assert_eq!(validator_stake.lamports, expected_lamports);
}
}
#[tokio::test]
async fn merge_transient_stake_after_remove() {
let (mut context, stake_pool_accounts, stake_accounts, lamports, reserve_lamports, mut slot) =
setup(1).await;
let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
let deactivated_lamports = lamports + stake_rent;
let new_authority = Pubkey::new_unique();
// Decrease and remove all validators
for stake_account in &stake_accounts {
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_account.stake_account,
&stake_account.transient_stake_account,
deactivated_lamports,
)
.await;
assert!(error.is_none());
let error = stake_pool_accounts
.remove_validator_from_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&new_authority,
&stake_account.stake_account,
&stake_account.transient_stake_account,
)
.await;
assert!(error.is_none());
}
// Warp forward to merge time
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
slot += slots_per_epoch;
context.warp_to_slot(slot).unwrap();
// Update without merge, status should be DeactivatingTransient
let error = stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
true,
)
.await;
assert!(error.is_none());
let validator_list = get_account(
&mut context.banks_client,
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let validator_list =
try_from_slice_unchecked::<ValidatorList>(validator_list.data.as_slice()).unwrap();
assert_eq!(validator_list.validators.len(), 1);
assert_eq!(
validator_list.validators[0].status,
StakeStatus::DeactivatingTransient
);
assert_eq!(
validator_list.validators[0].stake_lamports,
deactivated_lamports
);
// Update with merge, status should be ReadyForRemoval and no lamports
let error = stake_pool_accounts
.update_validator_list_balance(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
stake_accounts
.iter()
.map(|v| v.vote.pubkey())
.collect::<Vec<Pubkey>>()
.as_slice(),
false,
)
.await;
assert!(error.is_none());
let validator_list = get_account(
&mut context.banks_client,
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let validator_list =
try_from_slice_unchecked::<ValidatorList>(validator_list.data.as_slice()).unwrap();
assert_eq!(validator_list.validators.len(), 1);
assert_eq!(
validator_list.validators[0].status,
StakeStatus::ReadyForRemoval
);
assert_eq!(validator_list.validators[0].stake_lamports, 0);
let reserve_stake = context
.banks_client
.get_account(stake_pool_accounts.reserve_stake.pubkey())
.await
.unwrap()
.unwrap();
assert_eq!(
reserve_stake.lamports,
reserve_lamports + deactivated_lamports + stake_rent + 1
);
// Update stake pool balance, should be gone
let error = stake_pool_accounts
.update_stake_pool_balance(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
)
.await;
assert!(error.is_none());
let validator_list = get_account(
&mut context.banks_client,
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let validator_list =
try_from_slice_unchecked::<ValidatorList>(validator_list.data.as_slice()).unwrap();
assert_eq!(validator_list.validators.len(), 0);
}
#[tokio::test]
async fn fail_with_uninitialized_validator_list() {} // TODO
#[tokio::test]
async fn fail_with_wrong_stake_state() {} // TODO

View File

@ -19,7 +19,8 @@ use {
transport::TransportError, transport::TransportError,
}, },
spl_stake_pool::{ spl_stake_pool::{
borsh::try_from_slice_unchecked, error, id, instruction, stake_program, state, borsh::try_from_slice_unchecked, error::StakePoolError, id, instruction, stake_program,
state,
}, },
}; };
@ -29,63 +30,37 @@ async fn setup() -> (
Hash, Hash,
StakePoolAccounts, StakePoolAccounts,
ValidatorStakeAccount, ValidatorStakeAccount,
Keypair,
) { ) {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let user = Keypair::new(); let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
let user_stake = ValidatorStakeAccount::new_with_target_authority(
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.stake_pool.pubkey(),
);
user_stake user_stake
.create_and_delegate( .create_and_delegate(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.owner, &stake_pool_accounts.staker,
) )
.await; .await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
( (
banks_client, banks_client,
payer, payer,
recent_blockhash, recent_blockhash,
stake_pool_accounts, stake_pool_accounts,
user_stake, user_stake,
user_pool_account,
) )
} }
#[tokio::test] #[tokio::test]
async fn test_add_validator_to_pool() { async fn success() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
) = setup().await;
let error = stake_pool_accounts let error = stake_pool_accounts
.add_validator_to_pool( .add_validator_to_pool(
@ -93,28 +68,10 @@ async fn test_add_validator_to_pool() {
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake.stake_account, &user_stake.stake_account,
&user_pool_account.pubkey(),
) )
.await; .await;
assert!(error.is_none()); assert!(error.is_none());
let stake_account_balance = banks_client
.get_account(user_stake.stake_account)
.await
.unwrap()
.unwrap()
.lamports;
let deposit_tokens = stake_account_balance; // For now 1:1 math
// Check token account balance
let token_balance = get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
assert_eq!(token_balance, deposit_tokens);
let pool_fee_token_balance = get_token_balance(
&mut banks_client,
&stake_pool_accounts.pool_fee_account.pubkey(),
)
.await;
assert_eq!(pool_fee_token_balance, 0); // No fee when adding validator stake accounts
// Check if validator account was added to the list // Check if validator account was added to the list
let validator_list = get_account( let validator_list = get_account(
&mut banks_client, &mut banks_client,
@ -129,9 +86,10 @@ async fn test_add_validator_to_pool() {
account_type: state::AccountType::ValidatorList, account_type: state::AccountType::ValidatorList,
max_validators: stake_pool_accounts.max_validators, max_validators: stake_pool_accounts.max_validators,
validators: vec![state::ValidatorStakeInfo { validators: vec![state::ValidatorStakeInfo {
vote_account: user_stake.vote.pubkey(), status: state::StakeStatus::Active,
vote_account_address: user_stake.vote.pubkey(),
last_update_epoch: 0, last_update_epoch: 0,
balance: stake_account_balance, stake_lamports: 0,
}] }]
} }
); );
@ -155,105 +113,9 @@ async fn test_add_validator_to_pool() {
} }
#[tokio::test] #[tokio::test]
async fn test_add_validator_to_pool_with_wrong_token_program_id() { async fn fail_with_wrong_validator_list_account() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
) = setup().await;
let mut transaction = Transaction::new_with_payer(
&[instruction::add_validator_to_pool(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(),
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.withdraw_authority,
&stake_pool_accounts.validator_list.pubkey(),
&user_stake.stake_account,
&user_pool_account.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&stake_program::id(),
)
.unwrap()],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
let transaction_error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
assert_eq!(error, InstructionError::IncorrectProgramId);
}
_ => panic!("Wrong error occurs while try to add validator stake address with wrong token program ID"),
}
}
#[tokio::test]
async fn test_add_validator_to_pool_with_wrong_pool_mint_account() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
) = setup().await;
let wrong_pool_mint = Keypair::new();
let mut transaction = Transaction::new_with_payer(
&[instruction::add_validator_to_pool(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(),
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.withdraw_authority,
&stake_pool_accounts.validator_list.pubkey(),
&user_stake.stake_account,
&user_pool_account.pubkey(),
&wrong_pool_mint.pubkey(),
&spl_token::id(),
)
.unwrap()],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
let transaction_error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = error::StakePoolError::WrongPoolMint as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to add validator stake address with wrong pool mint account"),
}
}
#[tokio::test]
async fn test_add_validator_to_pool_with_wrong_validator_list_account() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
) = setup().await;
let wrong_validator_list = Keypair::new(); let wrong_validator_list = Keypair::new();
@ -261,19 +123,14 @@ async fn test_add_validator_to_pool_with_wrong_validator_list_account() {
&[instruction::add_validator_to_pool( &[instruction::add_validator_to_pool(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.withdraw_authority, &stake_pool_accounts.withdraw_authority,
&wrong_validator_list.pubkey(), &wrong_validator_list.pubkey(),
&user_stake.stake_account, &user_stake.stake_account,
&user_pool_account.pubkey(), )],
&stake_pool_accounts.pool_mint.pubkey(),
&spl_token::id(),
)
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -285,7 +142,7 @@ async fn test_add_validator_to_pool_with_wrong_validator_list_account() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::InvalidValidatorStakeList as u32; let program_error = StakePoolError::InvalidValidatorStakeList as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while try to add validator stake address with wrong validator stake list account"), _ => panic!("Wrong error occurs while try to add validator stake address with wrong validator stake list account"),
@ -293,15 +150,122 @@ async fn test_add_validator_to_pool_with_wrong_validator_list_account() {
} }
#[tokio::test] #[tokio::test]
async fn test_try_to_add_already_added_validator_stake_account() { async fn fail_too_little_stake() {
let ( let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
mut banks_client, let stake_pool_accounts = StakePoolAccounts::new();
payer, stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
create_vote(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.validator,
&user_stake.vote,
)
.await;
create_validator_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.stake_pool,
&stake_pool_accounts.staker,
&user_stake.stake_account,
&user_stake.vote.pubkey(),
)
.await;
// Create stake account to withdraw to
let split = Keypair::new();
create_blank_stake_account(&mut banks_client, &payer, &recent_blockhash, &split).await;
let transaction = Transaction::new_signed_with_payer(
&[stake_program::split_only(
&user_stake.stake_account,
&stake_pool_accounts.staker.pubkey(),
1,
&split.pubkey(),
)],
Some(&payer.pubkey()),
&[&payer, &stake_pool_accounts.staker],
recent_blockhash, recent_blockhash,
stake_pool_accounts, );
user_stake,
user_pool_account, banks_client.process_transaction(transaction).await.unwrap();
) = setup().await;
let error = stake_pool_accounts
.add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.stake_account,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
),
);
}
#[tokio::test]
async fn fail_too_much_stake() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
user_stake
.create_and_delegate(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.staker,
)
.await;
transfer(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.stake_account,
1,
)
.await;
let error = stake_pool_accounts
.add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.stake_account,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
),
);
}
#[tokio::test]
async fn fail_double_add() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
setup().await;
stake_pool_accounts stake_pool_accounts
.add_validator_to_pool( .add_validator_to_pool(
@ -309,7 +273,6 @@ async fn test_try_to_add_already_added_validator_stake_account() {
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake.stake_account, &user_stake.stake_account,
&user_pool_account.pubkey(),
) )
.await; .await;
@ -321,7 +284,6 @@ async fn test_try_to_add_already_added_validator_stake_account() {
&payer, &payer,
&latest_blockhash, &latest_blockhash,
&user_stake.stake_account, &user_stake.stake_account,
&user_pool_account.pubkey(),
) )
.await .await
.unwrap(); .unwrap();
@ -331,7 +293,7 @@ async fn test_try_to_add_already_added_validator_stake_account() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::ValidatorAlreadyAdded as u32; let program_error = StakePoolError::ValidatorAlreadyAdded as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while try to add already added validator stake account"), _ => panic!("Wrong error occurs while try to add already added validator stake account"),
@ -339,15 +301,9 @@ async fn test_try_to_add_already_added_validator_stake_account() {
} }
#[tokio::test] #[tokio::test]
async fn test_not_owner_try_to_add_validator_to_pool() { async fn fail_wrong_staker() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
) = setup().await;
let malicious = Keypair::new(); let malicious = Keypair::new();
@ -356,15 +312,10 @@ async fn test_not_owner_try_to_add_validator_to_pool() {
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&malicious.pubkey(), &malicious.pubkey(),
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.withdraw_authority, &stake_pool_accounts.withdraw_authority,
&stake_pool_accounts.validator_list.pubkey(), &stake_pool_accounts.validator_list.pubkey(),
&user_stake.stake_account, &user_stake.stake_account,
&user_pool_account.pubkey(), )],
&stake_pool_accounts.pool_mint.pubkey(),
&spl_token::id(),
)
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &malicious], recent_blockhash); transaction.sign(&[&payer, &malicious], recent_blockhash);
@ -379,7 +330,7 @@ async fn test_not_owner_try_to_add_validator_to_pool() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::WrongOwner as u32; let program_error = StakePoolError::WrongStaker as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while malicious try to add validator stake account"), _ => panic!("Wrong error occurs while malicious try to add validator stake account"),
@ -387,28 +338,18 @@ async fn test_not_owner_try_to_add_validator_to_pool() {
} }
#[tokio::test] #[tokio::test]
async fn test_not_owner_try_to_add_validator_to_pool_without_signature() { async fn fail_without_signature() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
) = setup().await;
let accounts = vec![ let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false), AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.deposit_authority, false),
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
AccountMeta::new(user_stake.stake_account, false), AccountMeta::new(user_stake.stake_account, false),
AccountMeta::new(user_pool_account.pubkey(), false),
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(sysvar::stake_history::id(), false), AccountMeta::new_readonly(sysvar::stake_history::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(stake_program::id(), false), AccountMeta::new_readonly(stake_program::id(), false),
]; ];
let instruction = Instruction { let instruction = Instruction {
@ -432,7 +373,7 @@ async fn test_not_owner_try_to_add_validator_to_pool_without_signature() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::SignatureMissing as u32; let program_error = StakePoolError::SignatureMissing as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while malicious try to add validator stake account without signing transaction"), _ => panic!("Wrong error occurs while malicious try to add validator stake account without signing transaction"),
@ -440,30 +381,20 @@ async fn test_not_owner_try_to_add_validator_to_pool_without_signature() {
} }
#[tokio::test] #[tokio::test]
async fn test_add_validator_to_pool_with_wrong_stake_program_id() { async fn fail_with_wrong_stake_program_id() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
) = setup().await;
let wrong_stake_program = Pubkey::new_unique(); let wrong_stake_program = Pubkey::new_unique();
let accounts = vec![ let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), true), AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true),
AccountMeta::new_readonly(stake_pool_accounts.deposit_authority, false),
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
AccountMeta::new(user_stake.stake_account, false), AccountMeta::new(user_stake.stake_account, false),
AccountMeta::new(user_pool_account.pubkey(), false),
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(sysvar::stake_history::id(), false), AccountMeta::new_readonly(sysvar::stake_history::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(wrong_stake_program, false), AccountMeta::new_readonly(wrong_stake_program, false),
]; ];
let instruction = Instruction { let instruction = Instruction {
@ -474,7 +405,7 @@ async fn test_add_validator_to_pool_with_wrong_stake_program_id() {
.unwrap(), .unwrap(),
}; };
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -492,64 +423,42 @@ async fn test_add_validator_to_pool_with_wrong_stake_program_id() {
} }
#[tokio::test] #[tokio::test]
async fn test_add_too_many_validator_stake_accounts() { async fn fail_add_too_many_validator_stake_accounts() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let mut stake_pool_accounts = StakePoolAccounts::new(); let mut stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts.max_validators = 1; stake_pool_accounts.max_validators = 1;
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let user = Keypair::new(); let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
let user_stake = ValidatorStakeAccount::new_with_target_authority(
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.stake_pool.pubkey(),
);
user_stake user_stake
.create_and_delegate( .create_and_delegate(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.owner, &stake_pool_accounts.staker,
) )
.await; .await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
let error = stake_pool_accounts let error = stake_pool_accounts
.add_validator_to_pool( .add_validator_to_pool(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake.stake_account, &user_stake.stake_account,
&user_pool_account.pubkey(),
) )
.await; .await;
assert!(error.is_none()); assert!(error.is_none());
let user_stake = ValidatorStakeAccount::new_with_target_authority( let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.stake_pool.pubkey(),
);
user_stake user_stake
.create_and_delegate( .create_and_delegate(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.owner, &stake_pool_accounts.staker,
) )
.await; .await;
let error = stake_pool_accounts let error = stake_pool_accounts
@ -558,7 +467,6 @@ async fn test_add_too_many_validator_stake_accounts() {
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake.stake_account, &user_stake.stake_account,
&user_pool_account.pubkey(),
) )
.await .await
.unwrap() .unwrap()
@ -570,7 +478,7 @@ async fn test_add_too_many_validator_stake_accounts() {
} }
#[tokio::test] #[tokio::test]
async fn test_add_validator_to_pool_to_unupdated_stake_pool() {} // TODO async fn fail_with_unupdated_stake_pool() {} // TODO
#[tokio::test] #[tokio::test]
async fn test_add_validator_to_pool_with_uninitialized_validator_list_account() {} // TODO async fn fail_with_uninitialized_validator_list_account() {} // TODO

View File

@ -26,16 +26,24 @@ async fn success_create_validator_stake_account() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let validator = Keypair::new(); let validator = Keypair::new();
create_vote(&mut banks_client, &payer, &recent_blockhash, &validator).await; let vote = Keypair::new();
create_vote(
&mut banks_client,
&payer,
&recent_blockhash,
&validator,
&vote,
)
.await;
let (stake_account, _) = find_stake_program_address( let (stake_account, _) = find_stake_program_address(
&id(), &id(),
&validator.pubkey(), &vote.pubkey(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
); );
@ -43,15 +51,14 @@ async fn success_create_validator_stake_account() {
&[instruction::create_validator_stake_account( &[instruction::create_validator_stake_account(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.staker.pubkey(),
&payer.pubkey(), &payer.pubkey(),
&stake_account, &stake_account,
&validator.pubkey(), &vote.pubkey(),
) )],
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap(); banks_client.process_transaction(transaction).await.unwrap();
// Check authorities // Check authorities
@ -59,12 +66,15 @@ async fn success_create_validator_stake_account() {
let stake_state = deserialize::<stake_program::StakeState>(&stake.data).unwrap(); let stake_state = deserialize::<stake_program::StakeState>(&stake.data).unwrap();
match stake_state { match stake_state {
stake_program::StakeState::Stake(meta, stake) => { stake_program::StakeState::Stake(meta, stake) => {
assert_eq!(&meta.authorized.staker, &stake_pool_accounts.owner.pubkey()); assert_eq!(
&meta.authorized.staker,
&stake_pool_accounts.staker.pubkey()
);
assert_eq!( assert_eq!(
&meta.authorized.withdrawer, &meta.authorized.withdrawer,
&stake_pool_accounts.owner.pubkey() &stake_pool_accounts.staker.pubkey()
); );
assert_eq!(stake.delegation.voter_pubkey, validator.pubkey()); assert_eq!(stake.delegation.voter_pubkey, vote.pubkey());
} }
_ => panic!(), _ => panic!(),
} }
@ -75,7 +85,7 @@ async fn fail_create_validator_stake_account_on_non_vote_account() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
@ -88,15 +98,14 @@ async fn fail_create_validator_stake_account_on_non_vote_account() {
&[instruction::create_validator_stake_account( &[instruction::create_validator_stake_account(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.staker.pubkey(),
&payer.pubkey(), &payer.pubkey(),
&stake_account, &stake_account,
&validator, &validator,
) )],
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -115,7 +124,7 @@ async fn fail_create_validator_stake_account_with_wrong_system_program() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
@ -126,7 +135,7 @@ async fn fail_create_validator_stake_account_with_wrong_system_program() {
let wrong_system_program = Pubkey::new_unique(); let wrong_system_program = Pubkey::new_unique();
let accounts = vec![ let accounts = vec![
AccountMeta::new_readonly(stake_pool_accounts.stake_pool.pubkey(), false), AccountMeta::new_readonly(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), true), AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true),
AccountMeta::new(payer.pubkey(), true), AccountMeta::new(payer.pubkey(), true),
AccountMeta::new(stake_account, false), AccountMeta::new(stake_account, false),
AccountMeta::new_readonly(validator, false), AccountMeta::new_readonly(validator, false),
@ -146,7 +155,7 @@ async fn fail_create_validator_stake_account_with_wrong_system_program() {
}; };
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -165,7 +174,7 @@ async fn fail_create_validator_stake_account_with_wrong_stake_program() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
@ -176,7 +185,7 @@ async fn fail_create_validator_stake_account_with_wrong_stake_program() {
let wrong_stake_program = Pubkey::new_unique(); let wrong_stake_program = Pubkey::new_unique();
let accounts = vec![ let accounts = vec![
AccountMeta::new_readonly(stake_pool_accounts.stake_pool.pubkey(), false), AccountMeta::new_readonly(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), true), AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true),
AccountMeta::new(payer.pubkey(), true), AccountMeta::new(payer.pubkey(), true),
AccountMeta::new(stake_account, false), AccountMeta::new(stake_account, false),
AccountMeta::new_readonly(validator, false), AccountMeta::new_readonly(validator, false),
@ -196,7 +205,7 @@ async fn fail_create_validator_stake_account_with_wrong_stake_program() {
}; };
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -215,7 +224,7 @@ async fn fail_create_validator_stake_account_with_incorrect_address() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
@ -226,15 +235,14 @@ async fn fail_create_validator_stake_account_with_incorrect_address() {
&[instruction::create_validator_stake_account( &[instruction::create_validator_stake_account(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.staker.pubkey(),
&payer.pubkey(), &payer.pubkey(),
&stake_account.pubkey(), &stake_account.pubkey(),
&validator, &validator,
) )],
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await

View File

@ -19,7 +19,8 @@ use {
transport::TransportError, transport::TransportError,
}, },
spl_stake_pool::{ spl_stake_pool::{
borsh::try_from_slice_unchecked, error, id, instruction, stake_program, state, borsh::try_from_slice_unchecked, error::StakePoolError, id, instruction, stake_program,
state,
}, },
}; };
@ -29,51 +30,30 @@ async fn setup() -> (
Hash, Hash,
StakePoolAccounts, StakePoolAccounts,
ValidatorStakeAccount, ValidatorStakeAccount,
Keypair,
Keypair,
) { ) {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 10_000_000_000)
.await .await
.unwrap(); .unwrap();
let user = Keypair::new(); let validator_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
validator_stake
let user_stake = ValidatorStakeAccount::new_with_target_authority(
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.stake_pool.pubkey(),
);
user_stake
.create_and_delegate( .create_and_delegate(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.owner, &stake_pool_accounts.staker,
) )
.await; .await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
let error = stake_pool_accounts let error = stake_pool_accounts
.add_validator_to_pool( .add_validator_to_pool(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake.stake_account, &validator_stake.stake_account,
&user_pool_account.pubkey(),
) )
.await; .await;
assert!(error.is_none()); assert!(error.is_none());
@ -83,35 +63,14 @@ async fn setup() -> (
payer, payer,
recent_blockhash, recent_blockhash,
stake_pool_accounts, stake_pool_accounts,
user_stake, validator_stake,
user_pool_account,
user,
) )
} }
#[tokio::test] #[tokio::test]
async fn test_remove_validator_from_pool() { async fn success() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
user,
) = setup().await;
let tokens_to_burn = get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
delegate_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account.pubkey(),
&user,
&stake_pool_accounts.withdraw_authority,
tokens_to_burn,
)
.await;
let new_authority = Pubkey::new_unique(); let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts let error = stake_pool_accounts
@ -119,17 +78,13 @@ async fn test_remove_validator_from_pool() {
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake.stake_account,
&user_pool_account.pubkey(),
&new_authority, &new_authority,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
) )
.await; .await;
assert!(error.is_none()); assert!(error.is_none());
// Check if all tokens were burned
let tokens_left = get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
assert_eq!(tokens_left, 0);
// Check if account was removed from the list of stake accounts // Check if account was removed from the list of stake accounts
let validator_list = get_account( let validator_list = get_account(
&mut banks_client, &mut banks_client,
@ -148,7 +103,7 @@ async fn test_remove_validator_from_pool() {
); );
// Check of stake account authority has changed // Check of stake account authority has changed
let stake = get_account(&mut banks_client, &user_stake.stake_account).await; let stake = get_account(&mut banks_client, &validator_stake.stake_account).await;
let stake_state = deserialize::<stake_program::StakeState>(&stake.data).unwrap(); let stake_state = deserialize::<stake_program::StakeState>(&stake.data).unwrap();
match stake_state { match stake_state {
stake_program::StakeState::Stake(meta, _) => { stake_program::StakeState::Stake(meta, _) => {
@ -160,31 +115,22 @@ async fn test_remove_validator_from_pool() {
} }
#[tokio::test] #[tokio::test]
async fn test_remove_validator_from_pool_with_wrong_stake_program_id() { async fn fail_with_wrong_stake_program_id() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
_,
) = setup().await;
let wrong_stake_program = Pubkey::new_unique(); let wrong_stake_program = Pubkey::new_unique();
let new_authority = Pubkey::new_unique(); let new_authority = Pubkey::new_unique();
let accounts = vec![ let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), true), AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true),
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
AccountMeta::new_readonly(new_authority, false), AccountMeta::new_readonly(new_authority, false),
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
AccountMeta::new(user_stake.stake_account, false), AccountMeta::new(validator_stake.stake_account, false),
AccountMeta::new(user_pool_account.pubkey(), false), AccountMeta::new_readonly(validator_stake.transient_stake_account, false),
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(wrong_stake_program, false), AccountMeta::new_readonly(wrong_stake_program, false),
]; ];
let instruction = Instruction { let instruction = Instruction {
@ -196,7 +142,7 @@ async fn test_remove_validator_from_pool_with_wrong_stake_program_id() {
}; };
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -215,112 +161,9 @@ async fn test_remove_validator_from_pool_with_wrong_stake_program_id() {
} }
#[tokio::test] #[tokio::test]
async fn test_remove_validator_from_pool_with_wrong_token_program_id() { async fn fail_with_wrong_validator_list_account() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
_,
) = setup().await;
let wrong_token_program = Keypair::new();
let new_authority = Pubkey::new_unique();
let mut transaction = Transaction::new_with_payer(
&[instruction::remove_validator_from_pool(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(),
&stake_pool_accounts.withdraw_authority,
&new_authority,
&stake_pool_accounts.validator_list.pubkey(),
&user_stake.stake_account,
&user_pool_account.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&wrong_token_program.pubkey(),
)
.unwrap()],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
let transaction_error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
assert_eq!(error, InstructionError::IncorrectProgramId);
}
_ => panic!("Wrong error occurs while try to remove validator stake address with wrong token program ID"),
}
}
#[tokio::test]
async fn test_remove_validator_from_pool_with_wrong_pool_mint_account() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
_,
) = setup().await;
let wrong_pool_mint = Keypair::new();
let new_authority = Pubkey::new_unique();
let mut transaction = Transaction::new_with_payer(
&[instruction::remove_validator_from_pool(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(),
&stake_pool_accounts.withdraw_authority,
&new_authority,
&stake_pool_accounts.validator_list.pubkey(),
&user_stake.stake_account,
&user_pool_account.pubkey(),
&wrong_pool_mint.pubkey(),
&spl_token::id(),
)
.unwrap()],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
let transaction_error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = error::StakePoolError::WrongPoolMint as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to remove validator stake address with wrong pool mint account"),
}
}
#[tokio::test]
async fn test_remove_validator_from_pool_with_wrong_validator_list_account() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
_,
) = setup().await;
let wrong_validator_list = Keypair::new(); let wrong_validator_list = Keypair::new();
@ -329,19 +172,16 @@ async fn test_remove_validator_from_pool_with_wrong_validator_list_account() {
&[instruction::remove_validator_from_pool( &[instruction::remove_validator_from_pool(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.owner.pubkey(), &stake_pool_accounts.staker.pubkey(),
&stake_pool_accounts.withdraw_authority, &stake_pool_accounts.withdraw_authority,
&new_authority, &new_authority,
&wrong_validator_list.pubkey(), &wrong_validator_list.pubkey(),
&user_stake.stake_account, &validator_stake.stake_account,
&user_pool_account.pubkey(), &validator_stake.transient_stake_account,
&stake_pool_accounts.pool_mint.pubkey(), )],
&spl_token::id(),
)
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash); transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -353,7 +193,7 @@ async fn test_remove_validator_from_pool_with_wrong_validator_list_account() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::InvalidValidatorStakeList as u32; let program_error = StakePoolError::InvalidValidatorStakeList as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while try to remove validator stake address with wrong validator stake list account"), _ => panic!("Wrong error occurs while try to remove validator stake address with wrong validator stake list account"),
@ -361,26 +201,16 @@ async fn test_remove_validator_from_pool_with_wrong_validator_list_account() {
} }
#[tokio::test] #[tokio::test]
async fn test_remove_already_removed_validator_stake_account() { async fn fail_not_at_minimum() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
user,
) = setup().await;
let tokens_to_burn = get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await; transfer(
delegate_tokens(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_pool_account.pubkey(), &validator_stake.stake_account,
&user, 1_000_001,
&stake_pool_accounts.withdraw_authority,
tokens_to_burn,
) )
.await; .await;
@ -390,9 +220,36 @@ async fn test_remove_already_removed_validator_stake_account() {
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake.stake_account,
&user_pool_account.pubkey(),
&new_authority, &new_authority,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
),
);
}
#[tokio::test]
async fn fail_double_remove() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
setup().await;
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.remove_validator_from_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&new_authority,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
) )
.await; .await;
assert!(error.is_none()); assert!(error.is_none());
@ -404,9 +261,9 @@ async fn test_remove_already_removed_validator_stake_account() {
&mut banks_client, &mut banks_client,
&payer, &payer,
&latest_blockhash, &latest_blockhash,
&user_stake.stake_account,
&user_pool_account.pubkey(),
&new_authority, &new_authority,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
) )
.await .await
.unwrap(); .unwrap();
@ -416,7 +273,7 @@ async fn test_remove_already_removed_validator_stake_account() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::ValidatorNotFound as u32; let program_error = StakePoolError::ValidatorNotFound as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => { _ => {
@ -426,16 +283,9 @@ async fn test_remove_already_removed_validator_stake_account() {
} }
#[tokio::test] #[tokio::test]
async fn test_not_owner_try_to_remove_validator_from_pool() { async fn fail_wrong_staker() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
_,
) = setup().await;
let malicious = Keypair::new(); let malicious = Keypair::new();
@ -448,12 +298,9 @@ async fn test_not_owner_try_to_remove_validator_from_pool() {
&stake_pool_accounts.withdraw_authority, &stake_pool_accounts.withdraw_authority,
&new_authority, &new_authority,
&stake_pool_accounts.validator_list.pubkey(), &stake_pool_accounts.validator_list.pubkey(),
&user_stake.stake_account, &validator_stake.stake_account,
&user_pool_account.pubkey(), &validator_stake.transient_stake_account,
&stake_pool_accounts.pool_mint.pubkey(), )],
&spl_token::id(),
)
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
); );
transaction.sign(&[&payer, &malicious], recent_blockhash); transaction.sign(&[&payer, &malicious], recent_blockhash);
@ -468,38 +315,31 @@ async fn test_not_owner_try_to_remove_validator_from_pool() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::WrongOwner as u32; let program_error = StakePoolError::WrongStaker as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while not an owner try to remove validator stake address"), _ => {
panic!("Wrong error occurs while not an staker try to remove validator stake address")
}
} }
} }
#[tokio::test] #[tokio::test]
async fn test_not_owner_try_to_remove_validator_from_pool_without_signature() { async fn fail_no_signature() {
let ( let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
mut banks_client, setup().await;
payer,
recent_blockhash,
stake_pool_accounts,
user_stake,
user_pool_account,
_,
) = setup().await;
let new_authority = Pubkey::new_unique(); let new_authority = Pubkey::new_unique();
let accounts = vec![ let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false), AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
AccountMeta::new_readonly(new_authority, false), AccountMeta::new_readonly(new_authority, false),
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
AccountMeta::new(user_stake.stake_account, false), AccountMeta::new(validator_stake.stake_account, false),
AccountMeta::new(user_pool_account.pubkey(), false), AccountMeta::new_readonly(validator_stake.transient_stake_account, false),
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(stake_program::id(), false), AccountMeta::new_readonly(stake_program::id(), false),
]; ];
let instruction = Instruction { let instruction = Instruction {
@ -510,8 +350,12 @@ async fn test_not_owner_try_to_remove_validator_from_pool_without_signature() {
.unwrap(), .unwrap(),
}; };
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); let transaction = Transaction::new_signed_with_payer(
transaction.sign(&[&payer], recent_blockhash); &[instruction],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -523,7 +367,7 @@ async fn test_not_owner_try_to_remove_validator_from_pool_without_signature() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::SignatureMissing as u32; let program_error = StakePoolError::SignatureMissing as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while malicious try to remove validator stake account without signing transaction"), _ => panic!("Wrong error occurs while malicious try to remove validator stake account without signing transaction"),
@ -531,7 +375,185 @@ async fn test_not_owner_try_to_remove_validator_from_pool_without_signature() {
} }
#[tokio::test] #[tokio::test]
async fn test_remove_validator_from_pool_from_unupdated_stake_pool() {} // TODO async fn fail_with_activating_transient_stake() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
setup().await;
// increase the validator stake
let error = stake_pool_accounts
.increase_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.transient_stake_account,
&validator_stake.vote.pubkey(),
2_000_000_000,
)
.await;
assert!(error.is_none());
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.remove_validator_from_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&new_authority,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
) => {
let program_error = StakePoolError::WrongStakeState as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while removing validator stake account while transient stake is activating"),
}
}
#[tokio::test] #[tokio::test]
async fn test_remove_validator_from_pool_with_uninitialized_validator_list_account() {} // TODO async fn success_with_deactivating_transient_stake() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
setup().await;
let rent = banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
let deposit_info = simple_deposit(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&validator_stake,
TEST_STAKE_AMOUNT,
)
.await
.unwrap();
// increase the validator stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
TEST_STAKE_AMOUNT + stake_rent,
)
.await;
assert!(error.is_none());
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.remove_validator_from_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&new_authority,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
)
.await;
assert!(error.is_none());
// fail deposit
let maybe_deposit = simple_deposit(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&validator_stake,
TEST_STAKE_AMOUNT,
)
.await;
assert!(maybe_deposit.is_none());
// fail withdraw
let user_stake_recipient = Keypair::new();
create_blank_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient,
)
.await;
let user_transfer_authority = Keypair::new();
let new_authority = Pubkey::new_unique();
delegate_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
1,
)
.await;
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.stake_account,
&new_authority,
1,
)
.await;
assert!(error.is_some());
// check validator has changed
let validator_list = get_account(
&mut banks_client,
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let validator_list =
try_from_slice_unchecked::<state::ValidatorList>(validator_list.data.as_slice()).unwrap();
let expected_list = state::ValidatorList {
account_type: state::AccountType::ValidatorList,
max_validators: stake_pool_accounts.max_validators,
validators: vec![state::ValidatorStakeInfo {
status: state::StakeStatus::DeactivatingTransient,
vote_account_address: validator_stake.vote.pubkey(),
last_update_epoch: 0,
stake_lamports: TEST_STAKE_AMOUNT + stake_rent,
}],
};
assert_eq!(validator_list, expected_list);
// Update, should not change, no merges yet
let error = stake_pool_accounts
.update_all(
&mut banks_client,
&payer,
&recent_blockhash,
&[validator_stake.vote.pubkey()],
false,
)
.await;
assert!(error.is_none());
let validator_list = get_account(
&mut banks_client,
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let validator_list =
try_from_slice_unchecked::<state::ValidatorList>(validator_list.data.as_slice()).unwrap();
assert_eq!(validator_list, expected_list);
}
#[tokio::test]
async fn fail_not_updated_stake_pool() {} // TODO
#[tokio::test]
async fn fail_with_uninitialized_validator_list_account() {} // TODO

View File

@ -3,6 +3,7 @@
mod helpers; mod helpers;
use { use {
bincode::deserialize,
borsh::{BorshDeserialize, BorshSerialize}, borsh::{BorshDeserialize, BorshSerialize},
helpers::*, helpers::*,
solana_program::{ solana_program::{
@ -18,7 +19,8 @@ use {
transport::TransportError, transport::TransportError,
}, },
spl_stake_pool::{ spl_stake_pool::{
borsh::try_from_slice_unchecked, error, id, instruction, stake_program, state, borsh::try_from_slice_unchecked, error::StakePoolError, id, instruction,
minimum_stake_lamports, stake_program, state,
}, },
spl_token::error::TokenError, spl_token::error::TokenError,
}; };
@ -29,17 +31,18 @@ async fn setup() -> (
Hash, Hash,
StakePoolAccounts, StakePoolAccounts,
ValidatorStakeAccount, ValidatorStakeAccount,
DepositInfo, DepositStakeAccount,
Keypair,
u64, u64,
) { ) {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let validator_stake_account: ValidatorStakeAccount = simple_add_validator_to_pool( let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
@ -47,25 +50,28 @@ async fn setup() -> (
) )
.await; .await;
let deposit_info: DepositInfo = simple_deposit( let deposit_info = simple_deposit(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts, &stake_pool_accounts,
&validator_stake_account, &validator_stake_account,
TEST_STAKE_AMOUNT,
) )
.await; .await
.unwrap();
let tokens_to_burn = deposit_info.pool_tokens / 4; let tokens_to_burn = deposit_info.pool_tokens / 4;
// Delegate tokens for burning // Delegate tokens for burning
let user_transfer_authority = Keypair::new();
delegate_tokens( delegate_tokens(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&deposit_info.user_pool_account, &deposit_info.pool_account.pubkey(),
&deposit_info.user, &deposit_info.authority,
&stake_pool_accounts.withdraw_authority, &user_transfer_authority.pubkey(),
tokens_to_burn, tokens_to_burn,
) )
.await; .await;
@ -77,12 +83,13 @@ async fn setup() -> (
stake_pool_accounts, stake_pool_accounts,
validator_stake_account, validator_stake_account,
deposit_info, deposit_info,
user_transfer_authority,
tokens_to_burn, tokens_to_burn,
) )
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_withdraw() { async fn success() {
let ( let (
mut banks_client, mut banks_client,
payer, payer,
@ -90,6 +97,7 @@ async fn test_stake_pool_withdraw() {
stake_pool_accounts, stake_pool_accounts,
validator_stake_account, validator_stake_account,
deposit_info, deposit_info,
user_transfer_authority,
tokens_to_burn, tokens_to_burn,
) = setup().await; ) = setup().await;
@ -123,33 +131,34 @@ async fn test_stake_pool_withdraw() {
// Save user token balance // Save user token balance
let user_token_balance_before = let user_token_balance_before =
get_token_balance(&mut banks_client, &deposit_info.user_pool_account).await; get_token_balance(&mut banks_client, &deposit_info.pool_account.pubkey()).await;
let new_authority = Pubkey::new_unique(); let new_authority = Pubkey::new_unique();
stake_pool_accounts let error = stake_pool_accounts
.withdraw_stake( .withdraw_stake(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&deposit_info.user_pool_account, &user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&new_authority, &new_authority,
tokens_to_burn, tokens_to_burn,
) )
.await .await;
.unwrap(); assert!(error.is_none());
// Check pool stats // Check pool stats
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap(); let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!( assert_eq!(
stake_pool.stake_total, stake_pool.total_stake_lamports,
stake_pool_before.stake_total - tokens_to_burn stake_pool_before.total_stake_lamports - tokens_to_burn
); );
assert_eq!( assert_eq!(
stake_pool.pool_total, stake_pool.pool_token_supply,
stake_pool_before.pool_total - tokens_to_burn stake_pool_before.pool_token_supply - tokens_to_burn
); );
// Check validator stake list storage // Check validator stake list storage
@ -164,13 +173,13 @@ async fn test_stake_pool_withdraw() {
.find(&validator_stake_account.vote.pubkey()) .find(&validator_stake_account.vote.pubkey())
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
validator_stake_item.balance, validator_stake_item.stake_lamports,
validator_stake_item_before.balance - tokens_to_burn validator_stake_item_before.stake_lamports - tokens_to_burn
); );
// Check tokens burned // Check tokens burned
let user_token_balance = let user_token_balance =
get_token_balance(&mut banks_client, &deposit_info.user_pool_account).await; get_token_balance(&mut banks_client, &deposit_info.pool_account.pubkey()).await;
assert_eq!( assert_eq!(
user_token_balance, user_token_balance,
user_token_balance_before - tokens_to_burn user_token_balance_before - tokens_to_burn
@ -179,9 +188,12 @@ async fn test_stake_pool_withdraw() {
// Check validator stake account balance // Check validator stake account balance
let validator_stake_account = let validator_stake_account =
get_account(&mut banks_client, &validator_stake_account.stake_account).await; get_account(&mut banks_client, &validator_stake_account.stake_account).await;
let stake_state =
deserialize::<stake_program::StakeState>(&validator_stake_account.data).unwrap();
let meta = stake_state.meta().unwrap();
assert_eq!( assert_eq!(
validator_stake_account.lamports, validator_stake_account.lamports - minimum_stake_lamports(&meta),
validator_stake_item.balance validator_stake_item.stake_lamports
); );
// Check user recipient stake account balance // Check user recipient stake account balance
@ -194,7 +206,7 @@ async fn test_stake_pool_withdraw() {
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_withdraw_with_wrong_stake_program() { async fn fail_with_wrong_stake_program() {
let ( let (
mut banks_client, mut banks_client,
payer, payer,
@ -202,6 +214,7 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() {
stake_pool_accounts, stake_pool_accounts,
validator_stake_account, validator_stake_account,
deposit_info, deposit_info,
user_transfer_authority,
tokens_to_burn, tokens_to_burn,
) = setup().await; ) = setup().await;
@ -218,7 +231,8 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() {
AccountMeta::new(validator_stake_account.stake_account, false), AccountMeta::new(validator_stake_account.stake_account, false),
AccountMeta::new(user_stake_recipient.pubkey(), false), AccountMeta::new(user_stake_recipient.pubkey(), false),
AccountMeta::new_readonly(new_authority, false), AccountMeta::new_readonly(new_authority, false),
AccountMeta::new(deposit_info.user_pool_account, false), AccountMeta::new_readonly(user_transfer_authority.pubkey(), true),
AccountMeta::new(deposit_info.pool_account.pubkey(), false),
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false), AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(spl_token::id(), false), AccountMeta::new_readonly(spl_token::id(), false),
@ -232,8 +246,12 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() {
.unwrap(), .unwrap(),
}; };
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); let transaction = Transaction::new_signed_with_payer(
transaction.sign(&[&payer], recent_blockhash); &[instruction],
Some(&payer.pubkey()),
&[&payer, &user_transfer_authority],
recent_blockhash,
);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -249,7 +267,7 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() {
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() { async fn fail_with_wrong_withdraw_authority() {
let ( let (
mut banks_client, mut banks_client,
payer, payer,
@ -257,6 +275,7 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
mut stake_pool_accounts, mut stake_pool_accounts,
validator_stake_account, validator_stake_account,
deposit_info, deposit_info,
user_transfer_authority,
tokens_to_burn, tokens_to_burn,
) = setup().await; ) = setup().await;
@ -272,13 +291,13 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&deposit_info.user_pool_account, &user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&new_authority, &new_authority,
tokens_to_burn, tokens_to_burn,
) )
.await .await
.err()
.unwrap(); .unwrap();
match transaction_error { match transaction_error {
@ -286,7 +305,7 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::InvalidProgramAddress as u32; let program_error = StakePoolError::InvalidProgramAddress as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while try to withdraw with wrong withdraw authority"), _ => panic!("Wrong error occurs while try to withdraw with wrong withdraw authority"),
@ -294,7 +313,7 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_withdraw_with_wrong_token_program_id() { async fn fail_with_wrong_token_program_id() {
let ( let (
mut banks_client, mut banks_client,
payer, payer,
@ -302,6 +321,7 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
stake_pool_accounts, stake_pool_accounts,
validator_stake_account, validator_stake_account,
deposit_info, deposit_info,
user_transfer_authority,
tokens_to_burn, tokens_to_burn,
) = setup().await; ) = setup().await;
@ -311,7 +331,7 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
let new_authority = Pubkey::new_unique(); let new_authority = Pubkey::new_unique();
let wrong_token_program = Keypair::new(); let wrong_token_program = Keypair::new();
let mut transaction = Transaction::new_with_payer( let transaction = Transaction::new_signed_with_payer(
&[instruction::withdraw( &[instruction::withdraw(
&id(), &id(),
&stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.stake_pool.pubkey(),
@ -320,15 +340,16 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&new_authority, &new_authority,
&deposit_info.user_pool_account, &user_transfer_authority.pubkey(),
&deposit_info.pool_account.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(), &stake_pool_accounts.pool_mint.pubkey(),
&wrong_token_program.pubkey(), &wrong_token_program.pubkey(),
tokens_to_burn, tokens_to_burn,
) )],
.unwrap()],
Some(&payer.pubkey()), Some(&payer.pubkey()),
&[&payer, &user_transfer_authority],
recent_blockhash,
); );
transaction.sign(&[&payer], recent_blockhash);
let transaction_error = banks_client let transaction_error = banks_client
.process_transaction(transaction) .process_transaction(transaction)
.await .await
@ -344,7 +365,7 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_withdraw_with_wrong_validator_list() { async fn fail_with_wrong_validator_list() {
let ( let (
mut banks_client, mut banks_client,
payer, payer,
@ -352,6 +373,7 @@ async fn test_stake_pool_withdraw_with_wrong_validator_list() {
mut stake_pool_accounts, mut stake_pool_accounts,
validator_stake_account, validator_stake_account,
deposit_info, deposit_info,
user_transfer_authority,
tokens_to_burn, tokens_to_burn,
) = setup().await; ) = setup().await;
@ -367,13 +389,13 @@ async fn test_stake_pool_withdraw_with_wrong_validator_list() {
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&deposit_info.user_pool_account, &user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&new_authority, &new_authority,
tokens_to_burn, tokens_to_burn,
) )
.await .await
.err()
.unwrap(); .unwrap();
match transaction_error { match transaction_error {
@ -381,7 +403,7 @@ async fn test_stake_pool_withdraw_with_wrong_validator_list() {
_, _,
InstructionError::Custom(error_index), InstructionError::Custom(error_index),
)) => { )) => {
let program_error = error::StakePoolError::InvalidValidatorStakeList as u32; let program_error = StakePoolError::InvalidValidatorStakeList as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!( _ => panic!(
@ -391,37 +413,36 @@ async fn test_stake_pool_withdraw_with_wrong_validator_list() {
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_withdraw_from_unknown_validator() { async fn fail_with_unknown_validator() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (
let stake_pool_accounts = StakePoolAccounts::new(); mut banks_client,
stake_pool_accounts payer,
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) recent_blockhash,
.await stake_pool_accounts,
.unwrap(); _,
_,
user_transfer_authority,
_,
) = setup().await;
let validator_stake_account = ValidatorStakeAccount::new_with_target_authority( let validator_stake_account =
&stake_pool_accounts.deposit_authority, ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
&stake_pool_accounts.stake_pool.pubkey(),
);
validator_stake_account validator_stake_account
.create_and_delegate( .create_and_delegate(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.owner, &stake_pool_accounts.staker,
) )
.await; .await;
let user_stake = ValidatorStakeAccount::new_with_target_authority( let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.stake_pool.pubkey(),
);
user_stake user_stake
.create_and_delegate( .create_and_delegate(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts.owner, &stake_pool_accounts.staker,
) )
.await; .await;
@ -453,6 +474,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() {
&user_stake, &user_stake,
&authorized, &authorized,
&lockup, &lockup,
TEST_STAKE_AMOUNT,
) )
.await; .await;
// make pool token account // make pool token account
@ -480,7 +502,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() {
&recent_blockhash, &recent_blockhash,
&user_pool_account, &user_pool_account,
&user, &user,
&stake_pool_accounts.withdraw_authority, &user_transfer_authority.pubkey(),
tokens_to_burn, tokens_to_burn,
) )
.await; .await;
@ -496,21 +518,19 @@ async fn test_stake_pool_withdraw_from_unknown_validator() {
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&user_transfer_authority,
&user_pool_account, &user_pool_account,
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&new_authority, &new_authority,
tokens_to_burn, tokens_to_burn,
) )
.await .await
.err() .unwrap()
.unwrap(); .unwrap();
match transaction_error { match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError( TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
_, let program_error = StakePoolError::ValidatorNotFound as u32;
InstructionError::Custom(error_index),
)) => {
let program_error = error::StakePoolError::ValidatorNotFound as u32;
assert_eq!(error_index, program_error); assert_eq!(error_index, program_error);
} }
_ => panic!("Wrong error occurs while try to do withdraw from unknown validator"), _ => panic!("Wrong error occurs while try to do withdraw from unknown validator"),
@ -518,7 +538,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() {
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_double_withdraw_to_the_same_account() { async fn fail_double_withdraw_to_the_same_account() {
let ( let (
mut banks_client, mut banks_client,
payer, payer,
@ -526,6 +546,7 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() {
stake_pool_accounts, stake_pool_accounts,
validator_stake_account, validator_stake_account,
deposit_info, deposit_info,
user_transfer_authority,
tokens_to_burn, tokens_to_burn,
) = setup().await; ) = setup().await;
@ -540,35 +561,48 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() {
.await; .await;
let new_authority = Pubkey::new_unique(); let new_authority = Pubkey::new_unique();
stake_pool_accounts let error = stake_pool_accounts
.withdraw_stake( .withdraw_stake(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&deposit_info.user_pool_account, &user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&new_authority, &new_authority,
tokens_to_burn, tokens_to_burn,
) )
.await .await;
.unwrap(); assert!(error.is_none());
let latest_blockhash = banks_client.get_recent_blockhash().await.unwrap(); let latest_blockhash = banks_client.get_recent_blockhash().await.unwrap();
// Delegate tokens for burning
delegate_tokens(
&mut banks_client,
&payer,
&latest_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
tokens_to_burn,
)
.await;
let transaction_error = stake_pool_accounts let transaction_error = stake_pool_accounts
.withdraw_stake( .withdraw_stake(
&mut banks_client, &mut banks_client,
&payer, &payer,
&latest_blockhash, &latest_blockhash,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&deposit_info.user_pool_account, &user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&new_authority, &new_authority,
tokens_to_burn, tokens_to_burn,
) )
.await .await
.err()
.unwrap(); .unwrap();
match transaction_error { match transaction_error {
@ -580,15 +614,15 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() {
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_withdraw_token_delegate_was_not_setup() { async fn fail_without_token_approval() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let validator_stake_account: ValidatorStakeAccount = simple_add_validator_to_pool( let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
@ -596,14 +630,16 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() {
) )
.await; .await;
let deposit_info: DepositInfo = simple_deposit( let deposit_info = simple_deposit(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts, &stake_pool_accounts,
&validator_stake_account, &validator_stake_account,
TEST_STAKE_AMOUNT,
) )
.await; .await
.unwrap();
let tokens_to_burn = deposit_info.pool_tokens / 4; let tokens_to_burn = deposit_info.pool_tokens / 4;
@ -618,19 +654,20 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() {
.await; .await;
let new_authority = Pubkey::new_unique(); let new_authority = Pubkey::new_unique();
let user_transfer_authority = Keypair::new();
let transaction_error = stake_pool_accounts let transaction_error = stake_pool_accounts
.withdraw_stake( .withdraw_stake(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&deposit_info.user_pool_account, &user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&new_authority, &new_authority,
tokens_to_burn, tokens_to_burn,
) )
.await .await
.err()
.unwrap(); .unwrap();
match transaction_error { match transaction_error {
@ -648,15 +685,15 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() {
} }
#[tokio::test] #[tokio::test]
async fn test_stake_pool_withdraw_with_low_delegation() { async fn fail_with_low_delegation() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new(); let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await .await
.unwrap(); .unwrap();
let validator_stake_account: ValidatorStakeAccount = simple_add_validator_to_pool( let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
@ -664,25 +701,28 @@ async fn test_stake_pool_withdraw_with_low_delegation() {
) )
.await; .await;
let deposit_info: DepositInfo = simple_deposit( let deposit_info = simple_deposit(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&stake_pool_accounts, &stake_pool_accounts,
&validator_stake_account, &validator_stake_account,
TEST_STAKE_AMOUNT,
) )
.await; .await
.unwrap();
let tokens_to_burn = deposit_info.pool_tokens / 4; let tokens_to_burn = deposit_info.pool_tokens / 4;
let user_transfer_authority = Keypair::new();
// Delegate tokens for burning // Delegate tokens for burning
delegate_tokens( delegate_tokens(
&mut banks_client, &mut banks_client,
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&deposit_info.user_pool_account, &deposit_info.pool_account.pubkey(),
&deposit_info.user, &deposit_info.authority,
&stake_pool_accounts.withdraw_authority, &user_transfer_authority.pubkey(),
1, 1,
) )
.await; .await;
@ -704,13 +744,13 @@ async fn test_stake_pool_withdraw_with_low_delegation() {
&payer, &payer,
&recent_blockhash, &recent_blockhash,
&user_stake_recipient.pubkey(), &user_stake_recipient.pubkey(),
&deposit_info.user_pool_account, &user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account, &validator_stake_account.stake_account,
&new_authority, &new_authority,
tokens_to_burn, tokens_to_burn,
) )
.await .await
.err()
.unwrap(); .unwrap();
match transaction_error { match transaction_error {
@ -726,3 +766,250 @@ async fn test_stake_pool_withdraw_with_low_delegation() {
), ),
} }
} }
#[tokio::test]
async fn fail_overdraw_validator() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
_validator_stake_account,
deposit_info,
user_transfer_authority,
tokens_to_burn,
) = setup().await;
// Create stake account to withdraw to
let user_stake_recipient = Keypair::new();
let _initial_stake_lamports = create_blank_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient,
)
.await;
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
tokens_to_burn,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
),
);
}
#[tokio::test]
async fn success_with_reserve() {
let mut context = program_test().start_with_context().await;
let stake_pool_accounts = StakePoolAccounts::new();
let initial_reserve_lamports = 1;
stake_pool_accounts
.initialize_stake_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
initial_reserve_lamports,
)
.await
.unwrap();
let validator_stake = simple_add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
)
.await;
let deposit_lamports = TEST_STAKE_AMOUNT;
let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
let deposit_info = simple_deposit(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
&validator_stake,
deposit_lamports,
)
.await
.unwrap();
// decrease some stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
deposit_lamports - 1,
)
.await;
assert!(error.is_none());
// warp forward to deactivation
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
context
.warp_to_slot(first_normal_slot + slots_per_epoch)
.unwrap();
// update to merge deactivated stake into reserve
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&[validator_stake.vote.pubkey()],
false,
)
.await;
// Delegate tokens for burning during withdraw
let user_transfer_authority = Keypair::new();
delegate_tokens(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
deposit_info.pool_tokens,
)
.await;
// Withdraw directly from reserve, fail because some stake left
let withdraw_destination = Keypair::new();
let withdraw_destination_authority = Pubkey::new_unique();
let initial_stake_lamports = create_blank_stake_account(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination,
)
.await;
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&withdraw_destination_authority,
deposit_info.pool_tokens,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
)
);
// decrease rest of stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
stake_rent + 1,
)
.await;
assert!(error.is_none());
// warp forward to deactivation
context
.warp_to_slot(first_normal_slot + 2 * slots_per_epoch)
.unwrap();
// update to merge deactivated stake into reserve
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&[validator_stake.vote.pubkey()],
false,
)
.await;
// now it works
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&withdraw_destination_authority,
deposit_info.pool_tokens,
)
.await;
assert!(error.is_none());
// Check tokens burned
let user_token_balance = get_token_balance(
&mut context.banks_client,
&deposit_info.pool_account.pubkey(),
)
.await;
assert_eq!(user_token_balance, 0);
// Check reserve stake account balance
let reserve_stake_account = get_account(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
)
.await;
let stake_state =
deserialize::<stake_program::StakeState>(&reserve_stake_account.data).unwrap();
let meta = stake_state.meta().unwrap();
assert_eq!(
initial_reserve_lamports + meta.rent_exempt_reserve,
reserve_stake_account.lamports
);
// Check user recipient stake account balance
let user_stake_recipient_account =
get_account(&mut context.banks_client, &withdraw_destination.pubkey()).await;
assert_eq!(
user_stake_recipient_account.lamports,
initial_stake_lamports + deposit_info.stake_lamports + stake_rent
);
}

View File

@ -11,13 +11,13 @@ exclude = ["js/**"]
[dependencies] [dependencies]
bincode = "1.3" bincode = "1.3"
borsh = "0.7.1" borsh = "0.8"
curve25519-dalek = {package = "curve25519-dalek-ng", git = "https://github.com/garious/curve25519-dalek", rev = "fcef1fa11b3d3e89a1abf8986386ba9ae375392c", default-features = false, features = ["borsh"]} curve25519-dalek = {package = "curve25519-dalek-ng", git = "https://github.com/garious/curve25519-dalek", rev = "fcef1fa11b3d3e89a1abf8986386ba9ae375392c", default-features = false, features = ["borsh"]}
elgamal_ristretto = { git = "https://github.com/garious/elgamal", rev = "892dbe115104bcb8cc26d79f9676c836ff6c018e", default-features = false } elgamal_ristretto = { git = "https://github.com/garious/elgamal", rev = "892dbe115104bcb8cc26d79f9676c836ff6c018e", default-features = false }
futures = "0.3" futures = "0.3"
solana-banks-client = "1.6.2" solana-banks-client = "1.6.7"
solana-cli-config = "1.6.2" solana-cli-config = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
spl-themis-ristretto = { version = "0.1.0", path = "../program_ristretto", features = ["no-entrypoint"] } spl-themis-ristretto = { version = "0.1.0", path = "../program_ristretto", features = ["no-entrypoint"] }
tarpc = { version = "0.22.0", features = ["full"] } tarpc = { version = "0.22.0", features = ["full"] }
tokio = "0.3" tokio = "0.3"
@ -25,11 +25,11 @@ url = "2.1"
[dev-dependencies] [dev-dependencies]
separator = "0.4.1" separator = "0.4.1"
solana-banks-server = "1.6.2" solana-banks-server = "1.6.7"
solana-bpf-loader-program = "1.6.2" solana-bpf-loader-program = "1.6.7"
solana-core = "1.6.2" solana-core = "1.6.7"
solana_rbpf = "0.1" solana_rbpf = "0.1"
solana-runtime = "1.6.2" solana-runtime = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -20,7 +20,7 @@ getrandom = { version = "0.1.15", features = ["dummy"] }
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
rand = "0.8.0" rand = "0.8.0"
solana-program = "1.6.2" solana-program = "1.6.7"
subtle = "=2.2.3" subtle = "=2.2.3"
thiserror = "1.0" thiserror = "1.0"

View File

@ -1,15 +0,0 @@
[package]
name = "spl-token-lending-client"
version = "0.1.0"
description = "Solana Program Library Token Lending Client"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2018"
[dependencies]
solana-client = "1.6.2"
solana-program = "1.6.2"
solana-sdk = "1.6.2"
spl-token-lending = { path = "../program", features = [ "no-entrypoint" ] }
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }

View File

@ -1,304 +0,0 @@
use solana_client::rpc_client::RpcClient;
use solana_program::program_pack::Pack;
use solana_sdk::{
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer},
system_instruction::create_account,
transaction::Transaction,
};
use spl_token::{
instruction::approve,
state::{Account as Token, Mint},
};
use spl_token_lending::{
instruction::{init_lending_market, init_reserve},
state::{LendingMarket, Reserve, ReserveConfig, ReserveFees},
};
use std::str::FromStr;
// -------- UPDATE START -------
const KEYPAIR_PATH: &str = "/your/path";
const SRM_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS";
const USDC_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS";
const WRAPPED_SOL_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS";
solana_program::declare_id!("TokenLend1ng1111111111111111111111111111111");
// -------- UPDATE END ---------
pub struct DexMarket {
pub name: &'static str,
pub pubkey: Pubkey,
}
pub fn main() {
let mut client = RpcClient::new("https://api.mainnet-beta.solana.com".to_owned());
let payer = read_keypair_file(&format!("{}/payer.json", KEYPAIR_PATH)).unwrap();
let usdc_mint_pubkey =
Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap();
let sol_usdc_dex_market = DexMarket {
name: "sol_usdc",
pubkey: Pubkey::from_str("9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT").unwrap(),
};
let srm_usdc_dex_market = DexMarket {
name: "srm_usdc",
pubkey: Pubkey::from_str("ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA").unwrap(),
};
let quote_token_mint = usdc_mint_pubkey;
let (lending_market_owner, lending_market_pubkey, _lending_market) =
create_lending_market(&mut client, quote_token_mint, &payer);
let usdc_liquidity_source = Pubkey::from_str(USDC_TOKEN_ACCOUNT).unwrap();
let usdc_reserve_config = ReserveConfig {
optimal_utilization_rate: 80,
loan_to_value_ratio: 75,
liquidation_bonus: 5,
liquidation_threshold: 80,
min_borrow_rate: 0,
optimal_borrow_rate: 4,
max_borrow_rate: 30,
fees: ReserveFees {
borrow_fee_wad: 100_000_000_000_000, // 1 bp
host_fee_percentage: 20,
},
};
let (usdc_reserve_pubkey, _usdc_reserve) = create_reserve(
&mut client,
usdc_reserve_config,
lending_market_pubkey,
&lending_market_owner,
None,
usdc_liquidity_source,
&payer,
);
println!("Created usdc reserve with pubkey: {}", usdc_reserve_pubkey);
let sol_liquidity_source = Pubkey::from_str(WRAPPED_SOL_TOKEN_ACCOUNT).unwrap();
let sol_reserve_config = ReserveConfig {
optimal_utilization_rate: 0,
loan_to_value_ratio: 75,
liquidation_bonus: 10,
liquidation_threshold: 80,
min_borrow_rate: 0,
optimal_borrow_rate: 2,
max_borrow_rate: 15,
fees: ReserveFees {
borrow_fee_wad: 1_000_000_000_000, // 0.01 bp
host_fee_percentage: 20,
},
};
let (sol_reserve_pubkey, _sol_reserve) = create_reserve(
&mut client,
sol_reserve_config,
lending_market_pubkey,
&lending_market_owner,
Some(sol_usdc_dex_market.pubkey),
sol_liquidity_source,
&payer,
);
println!("Created sol reserve with pubkey: {}", sol_reserve_pubkey);
let srm_liquidity_source = Pubkey::from_str(SRM_TOKEN_ACCOUNT).unwrap();
let srm_reserve_config = ReserveConfig {
optimal_utilization_rate: 0,
loan_to_value_ratio: 75,
liquidation_bonus: 10,
liquidation_threshold: 80,
min_borrow_rate: 0,
optimal_borrow_rate: 2,
max_borrow_rate: 15,
fees: ReserveFees {
borrow_fee_wad: 10_000_000_000_000, // 0.1 bp
host_fee_percentage: 25,
},
};
let (srm_reserve_pubkey, _srm_reserve) = create_reserve(
&mut client,
srm_reserve_config,
lending_market_pubkey,
&lending_market_owner,
Some(srm_usdc_dex_market.pubkey),
srm_liquidity_source,
&payer,
);
println!("Created srm reserve with pubkey: {}", srm_reserve_pubkey);
}
pub fn create_lending_market(
client: &mut RpcClient,
quote_token_mint: Pubkey,
payer: &Keypair,
) -> (Keypair, Pubkey, LendingMarket) {
let owner = read_keypair_file(&format!("{}/lending_market_owner.json", KEYPAIR_PATH)).unwrap();
let keypair = Keypair::new();
let pubkey = keypair.pubkey();
let mut transaction = Transaction::new_with_payer(
&[
create_account(
&payer.pubkey(),
&pubkey,
client
.get_minimum_balance_for_rent_exemption(LendingMarket::LEN)
.unwrap(),
LendingMarket::LEN as u64,
&id(),
),
init_lending_market(id(), pubkey, owner.pubkey(), quote_token_mint),
],
Some(&payer.pubkey()),
);
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&[&payer, &keypair], recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let account = client.get_account(&pubkey).unwrap();
let lending_market = LendingMarket::unpack(&account.data).unwrap();
(owner, pubkey, lending_market)
}
pub fn create_reserve(
client: &mut RpcClient,
config: ReserveConfig,
lending_market_pubkey: Pubkey,
lending_market_owner: &Keypair,
dex_market_pubkey: Option<Pubkey>,
liquidity_source_pubkey: Pubkey,
payer: &Keypair,
) -> (Pubkey, Reserve) {
let reserve_keypair = Keypair::new();
let reserve_pubkey = reserve_keypair.pubkey();
let collateral_mint_keypair = Keypair::new();
let collateral_supply_keypair = Keypair::new();
let collateral_fees_receiver_keypair = Keypair::new();
let liquidity_supply_keypair = Keypair::new();
let user_collateral_token_keypair = Keypair::new();
let user_transfer_authority = Keypair::new();
let liquidity_source_account = client.get_account(&liquidity_source_pubkey).unwrap();
let liquidity_source_token = Token::unpack(&liquidity_source_account.data).unwrap();
let liquidity_mint_pubkey = liquidity_source_token.mint;
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
let token_balance = client
.get_minimum_balance_for_rent_exemption(Token::LEN)
.unwrap();
let mut transaction = Transaction::new_with_payer(
&[
create_account(
&payer.pubkey(),
&collateral_mint_keypair.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&spl_token::id(),
),
create_account(
&payer.pubkey(),
&collateral_supply_keypair.pubkey(),
token_balance,
Token::LEN as u64,
&spl_token::id(),
),
create_account(
&payer.pubkey(),
&collateral_fees_receiver_keypair.pubkey(),
token_balance,
Token::LEN as u64,
&spl_token::id(),
),
create_account(
&payer.pubkey(),
&liquidity_supply_keypair.pubkey(),
token_balance,
Token::LEN as u64,
&spl_token::id(),
),
create_account(
&payer.pubkey(),
&user_collateral_token_keypair.pubkey(),
token_balance,
Token::LEN as u64,
&spl_token::id(),
),
create_account(
&payer.pubkey(),
&reserve_pubkey,
client
.get_minimum_balance_for_rent_exemption(Reserve::LEN)
.unwrap(),
Reserve::LEN as u64,
&id(),
),
],
Some(&payer.pubkey()),
);
transaction.sign(
&vec![
payer,
&reserve_keypair,
&collateral_mint_keypair,
&collateral_supply_keypair,
&liquidity_supply_keypair,
&user_collateral_token_keypair,
],
recent_blockhash,
);
client.send_and_confirm_transaction(&transaction).unwrap();
let mut transaction = Transaction::new_with_payer(
&[
approve(
&spl_token::id(),
&liquidity_source_pubkey,
&user_transfer_authority.pubkey(),
&payer.pubkey(),
&[],
liquidity_source_token.amount,
)
.unwrap(),
init_reserve(
id(),
liquidity_source_token.amount,
config,
liquidity_source_pubkey,
user_collateral_token_keypair.pubkey(),
reserve_pubkey,
liquidity_mint_pubkey,
liquidity_supply_keypair.pubkey(),
collateral_mint_keypair.pubkey(),
collateral_supply_keypair.pubkey(),
collateral_fees_receiver_keypair.pubkey(),
lending_market_pubkey,
lending_market_owner.pubkey(),
user_transfer_authority.pubkey(),
dex_market_pubkey,
),
],
Some(&payer.pubkey()),
);
transaction.sign(
&vec![payer, &lending_market_owner, &user_transfer_authority],
recent_blockhash,
);
client.send_and_confirm_transaction(&transaction).unwrap();
let account = client.get_account(&reserve_pubkey).unwrap();
(reserve_pubkey, Reserve::unpack(&account.data).unwrap())
}

View File

@ -0,0 +1,63 @@
# Flash Loan Design
We added a new instruction with the following signature for flash loan:
```rust
pub enum LendingInstruction {
// ....
/// Make a flash loan.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` Source liquidity token account.
/// Minted by reserve liquidity mint.
/// Must match the reserve liquidity supply.
/// 1. `[writable]` Destination liquidity token account.
/// Minted by reserve liquidity mint.
/// 2. `[writable]` Reserve account.
/// 3. `[]` Lending market account.
/// 4. `[]` Derived lending market authority.
/// 5. `[]` Flash loan receiver program account.
/// Must implement an instruction that has tag of 0 and a signature of `(repay_amount: u64)`
/// This instruction must return the amount to the source liquidity account.
/// 6. `[]` Token program id.
/// 7. `[writable]` Flash loan fee receiver account.
/// Must match the reserve liquidity fee receiver.
/// 8. `[writable]` Host fee receiver.
/// .. `[any]` Additional accounts expected by the receiving program's `ReceiveFlashLoan` instruction.
FlashLoan {
/// The amount that is to be borrowed
amount: u64,
},
}
```
In the implementation, we do the following in order:
1. Perform safety checks and calculate fees
2. Transfer `amount` from the source liquidity account to the destination liquidity account
2. Call the `ReceiveFlashLoan` function (the flash loan receiver program is required to have this function with tag `0`).
The additional account required for `ReceiveFlashLoan` is given from the 10th account of the `FlashLoan` instruction, i.e. after host fee receiver.
3. Check that the returned amount with the fee is in the reserve account after the completion of `ReceiveFlashLoan` function.
The flash loan receiver program should have a `ReceiveFlashLoan` instruction which executes the user-defined operation and return the funds to the reserve in the end.
```rust
pub enum FlashLoanReceiverInstruction {
/// Receive a flash loan and perform user-defined operation and finally return the fund back.
///
/// Accounts expected:
///
/// 0. `[writable]` Source liquidity (matching the destination from above).
/// 1. `[writable]` Destination liquidity (matching the source from above).
/// 2. `[]` Token program id
/// .. `[any]` Additional accounts provided to the lending program's `FlashLoan` instruction above.
ReceiveFlashLoan {
// Amount that is loaned to the receiver program
amount: u64
}
}
```
You can view a sample implementation [here](https://github.com/solana-labs/solana-program-library/tree/master/token-lending/program/tests/helpers/flash_loan_receiver.rs).

View File

@ -30,7 +30,7 @@ export const LendingMarketLayout: typeof BufferLayout.Structure = BufferLayout.s
Layout.publicKey("owner"), Layout.publicKey("owner"),
Layout.publicKey("quoteTokenMint"), Layout.publicKey("quoteTokenMint"),
Layout.publicKey("tokenProgramId"), Layout.publicKey("tokenProgramId"),
BufferLayout.blob(62, "padding"), BufferLayout.blob(128, "padding"),
] ]
); );

View File

@ -24,20 +24,33 @@
} }
}, },
"@babel/helper-validator-identifier": { "@babel/helper-validator-identifier": {
"version": "7.12.11", "version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
"dev": true "dev": true
}, },
"@babel/highlight": { "@babel/highlight": {
"version": "7.13.10", "version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
"integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.12.11", "@babel/helper-validator-identifier": "^7.14.0",
"chalk": "^2.0.0", "chalk": "^2.0.0",
"js-tokens": "^4.0.0" "js-tokens": "^4.0.0"
},
"dependencies": {
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
}
} }
}, },
"@babel/runtime": { "@babel/runtime": {
@ -49,9 +62,9 @@
} }
}, },
"@eslint/eslintrc": { "@eslint/eslintrc": {
"version": "0.4.0", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.1.tgz",
"integrity": "sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==", "integrity": "sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"ajv": "^6.12.4", "ajv": "^6.12.4",
@ -118,9 +131,9 @@
} }
}, },
"@rollup/plugin-commonjs": { "@rollup/plugin-commonjs": {
"version": "18.0.0", "version": "19.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-18.0.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-19.0.0.tgz",
"integrity": "sha512-fj92shhg8luw7XbA0HowAqz90oo7qtLGwqTKbyZ8pmOyH8ui5e+u0wPEgeHLH3djcVma6gUCUrjY6w5R2o1u6g==", "integrity": "sha512-adTpD6ATGbehdaQoZQ6ipDFhdjqsTgpOAhFiPwl+dzre4pPshsecptDPyEFb61JMJ1+mGljktaC4jI8ARMSNyw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@rollup/pluginutils": "^3.1.0", "@rollup/pluginutils": "^3.1.0",
@ -173,49 +186,18 @@
"dev": true "dev": true
}, },
"@solana/spl-token": { "@solana/spl-token": {
"version": "0.1.3", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.4.tgz",
"integrity": "sha512-M251on5RDz8VQXoKoQPeLANEyI4qhThKLZBeUiLbFZ93KRgouGfmV5D/bUZXkLF75PlLcARIzU9ptoHOlZ6SbQ==", "integrity": "sha512-W8uSC4ysWVjbKK7lvsIHFxdMIkOCEoOm9tYY9VVpBlUIp4+K5bpPxHXUlxMiHfkKPWAxab6IGUn71VVLg8uq5Q==",
"requires": { "requires": {
"@babel/runtime": "^7.10.5", "@babel/runtime": "^7.10.5",
"@solana/web3.js": "^1.2.2", "@solana/web3.js": "^1.9.1",
"bn.js": "^5.1.0", "bn.js": "^5.1.0",
"buffer": "6.0.3", "buffer": "6.0.3",
"buffer-layout": "^1.2.0", "buffer-layout": "^1.2.0",
"dotenv": "8.2.0" "dotenv": "8.2.0"
}, },
"dependencies": { "dependencies": {
"@solana/web3.js": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.2.4.tgz",
"integrity": "sha512-qTYmZiFPrk6ZWniIzLCwa/4+Ovlg6DXLc32Q1SPqqQldnnoZkrCBuZq8mcd8VkPC2pVAwJhxsLTY3I9i5O8tuQ==",
"requires": {
"@babel/runtime": "^7.12.5",
"bn.js": "^5.0.0",
"bs58": "^4.0.1",
"buffer": "6.0.1",
"buffer-layout": "^1.2.0",
"crypto-hash": "^1.2.2",
"jayson": "^3.4.4",
"js-sha3": "^0.8.0",
"node-fetch": "^2.6.1",
"rpc-websockets": "^7.4.2",
"secp256k1": "^4.0.2",
"superstruct": "^0.14.2",
"tweetnacl": "^1.0.0"
},
"dependencies": {
"buffer": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.1.tgz",
"integrity": "sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
}
}
},
"buffer": { "buffer": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@ -224,13 +206,18 @@
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"ieee754": "^1.2.1" "ieee754": "^1.2.1"
} }
},
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
} }
} }
}, },
"@solana/web3.js": { "@solana/web3.js": {
"version": "1.2.5", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.2.5.tgz", "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.10.1.tgz",
"integrity": "sha512-dkKIDhmSM9qX5eJs4bNWPLMLzWcSCd36Imj4IIIfJHKwMfhjO6sWfYjHD4dzB502GoqpvZ/jHv4JBxZR5ERSiA==", "integrity": "sha512-5zepm+AaVfC6uao/WxKT7SlemqgGEp+x7XUv+018RURIx+XEcF+G2ZgjrIbjy5OoXJyfz+wGw8Or2fxsypderA==",
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"bn.js": "^5.0.0", "bn.js": "^5.0.0",
@ -263,17 +250,17 @@
} }
}, },
"@types/connect": { "@types/connect": {
"version": "3.4.33", "version": "3.4.34",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==",
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/eslint": { "@types/eslint": {
"version": "7.2.8", "version": "7.2.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.8.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.10.tgz",
"integrity": "sha512-RTKvBsfz0T8CKOGZMfuluDNyMFHnu5lvNr4hWEsQeHXH6FcmIDIozOyWMh36nLGMwVd5UFNXC2xztA8lln22MQ==", "integrity": "sha512-kUEPnMKrqbtpCq/KTaGFFKAcz6Ethm2EjCoKIDaCmfRBWLbFuTcOJfTlorwbnboXBzahqWLgUp1BQeKHiJzPUQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/estree": "*", "@types/estree": "*",
@ -296,9 +283,9 @@
"dev": true "dev": true
}, },
"@types/express-serve-static-core": { "@types/express-serve-static-core": {
"version": "4.17.13", "version": "4.17.19",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz",
"integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==", "integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==",
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
"@types/qs": "*", "@types/qs": "*",
@ -312,9 +299,9 @@
"dev": true "dev": true
}, },
"@types/lodash": { "@types/lodash": {
"version": "4.14.165", "version": "4.14.168",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
"integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==" "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
}, },
"@types/mkdirp": { "@types/mkdirp": {
"version": "1.0.1", "version": "1.0.1",
@ -335,9 +322,9 @@
} }
}, },
"@types/node": { "@types/node": {
"version": "14.14.37", "version": "15.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
"integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==" "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA=="
}, },
"@types/prettier": { "@types/prettier": {
"version": "2.2.3", "version": "2.2.3",
@ -346,9 +333,9 @@
"dev": true "dev": true
}, },
"@types/qs": { "@types/qs": {
"version": "6.9.5", "version": "6.9.6",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz",
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA=="
}, },
"@types/range-parser": { "@types/range-parser": {
"version": "1.2.3", "version": "1.2.3",
@ -387,172 +374,92 @@
} }
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "4.20.0", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.23.0.tgz",
"integrity": "sha512-sw+3HO5aehYqn5w177z2D82ZQlqHCwcKSMboueo7oE4KU9QiC0SAgfS/D4z9xXvpTc8Bt41Raa9fBR8T2tIhoQ==", "integrity": "sha512-tGK1y3KIvdsQEEgq6xNn1DjiFJtl+wn8JJQiETtCbdQxw1vzjXyAaIkEmO2l6Nq24iy3uZBMFQjZ6ECf1QdgGw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/experimental-utils": "4.20.0", "@typescript-eslint/experimental-utils": "4.23.0",
"@typescript-eslint/scope-manager": "4.20.0", "@typescript-eslint/scope-manager": "4.23.0",
"debug": "^4.1.1", "debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"regexpp": "^3.0.0", "regexpp": "^3.0.0",
"semver": "^7.3.2", "semver": "^7.3.2",
"tsutils": "^3.17.1" "tsutils": "^3.17.1"
},
"dependencies": {
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
} }
}, },
"@typescript-eslint/experimental-utils": { "@typescript-eslint/experimental-utils": {
"version": "4.20.0", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.23.0.tgz",
"integrity": "sha512-sQNlf6rjLq2yB5lELl3gOE7OuoA/6IVXJUJ+Vs7emrQMva14CkOwyQwD7CW+TkmOJ4Q/YGmoDLmbfFrpGmbKng==", "integrity": "sha512-WAFNiTDnQfrF3Z2fQ05nmCgPsO5o790vOhmWKXbbYQTO9erE1/YsFot5/LnOUizLzU2eeuz6+U/81KV5/hFTGA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/json-schema": "^7.0.3", "@types/json-schema": "^7.0.3",
"@typescript-eslint/scope-manager": "4.20.0", "@typescript-eslint/scope-manager": "4.23.0",
"@typescript-eslint/types": "4.20.0", "@typescript-eslint/types": "4.23.0",
"@typescript-eslint/typescript-estree": "4.20.0", "@typescript-eslint/typescript-estree": "4.23.0",
"eslint-scope": "^5.0.0", "eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0" "eslint-utils": "^2.0.0"
} }
}, },
"@typescript-eslint/parser": { "@typescript-eslint/parser": {
"version": "4.20.0", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.23.0.tgz",
"integrity": "sha512-m6vDtgL9EABdjMtKVw5rr6DdeMCH3OA1vFb0dAyuZSa3e5yw1YRzlwFnm9knma9Lz6b2GPvoNSa8vOXrqsaglA==", "integrity": "sha512-wsvjksHBMOqySy/Pi2Q6UuIuHYbgAMwLczRl4YanEPKW5KVxI9ZzDYh3B5DtcZPQTGRWFJrfcbJ6L01Leybwug==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/scope-manager": "4.20.0", "@typescript-eslint/scope-manager": "4.23.0",
"@typescript-eslint/types": "4.20.0", "@typescript-eslint/types": "4.23.0",
"@typescript-eslint/typescript-estree": "4.20.0", "@typescript-eslint/typescript-estree": "4.23.0",
"debug": "^4.1.1" "debug": "^4.1.1"
},
"dependencies": {
"@typescript-eslint/scope-manager": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz",
"integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.20.0",
"@typescript-eslint/visitor-keys": "4.20.0"
}
},
"@typescript-eslint/types": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz",
"integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz",
"integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.20.0",
"@typescript-eslint/visitor-keys": "4.20.0",
"debug": "^4.1.1",
"globby": "^11.0.1",
"is-glob": "^4.0.1",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
}
},
"@typescript-eslint/visitor-keys": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz",
"integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.20.0",
"eslint-visitor-keys": "^2.0.0"
}
},
"eslint-visitor-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
"dev": true
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
} }
}, },
"@typescript-eslint/scope-manager": { "@typescript-eslint/scope-manager": {
"version": "4.20.0", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.23.0.tgz",
"integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==", "integrity": "sha512-ZZ21PCFxPhI3n0wuqEJK9omkw51wi2bmeKJvlRZPH5YFkcawKOuRMQMnI8mH6Vo0/DoHSeZJnHiIx84LmVQY+w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.20.0", "@typescript-eslint/types": "4.23.0",
"@typescript-eslint/visitor-keys": "4.20.0" "@typescript-eslint/visitor-keys": "4.23.0"
} }
}, },
"@typescript-eslint/types": { "@typescript-eslint/types": {
"version": "4.20.0", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.23.0.tgz",
"integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==", "integrity": "sha512-oqkNWyG2SLS7uTWLZf6Sr7Dm02gA5yxiz1RP87tvsmDsguVATdpVguHr4HoGOcFOpCvx9vtCSCyQUGfzq28YCw==",
"dev": true "dev": true
}, },
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "4.20.0", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.23.0.tgz",
"integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==", "integrity": "sha512-5Sty6zPEVZF5fbvrZczfmLCOcby3sfrSPu30qKoY1U3mca5/jvU5cwsPb/CO6Q3ByRjixTMIVsDkqwIxCf/dMw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.20.0", "@typescript-eslint/types": "4.23.0",
"@typescript-eslint/visitor-keys": "4.20.0", "@typescript-eslint/visitor-keys": "4.23.0",
"debug": "^4.1.1", "debug": "^4.1.1",
"globby": "^11.0.1", "globby": "^11.0.1",
"is-glob": "^4.0.1", "is-glob": "^4.0.1",
"semver": "^7.3.2", "semver": "^7.3.2",
"tsutils": "^3.17.1" "tsutils": "^3.17.1"
},
"dependencies": {
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
} }
}, },
"@typescript-eslint/visitor-keys": { "@typescript-eslint/visitor-keys": {
"version": "4.20.0", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.23.0.tgz",
"integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==", "integrity": "sha512-5PNe5cmX9pSifit0H+nPoQBXdbNzi5tOEec+3riK+ku4e3er37pKxMKDH5Ct5Y4fhWxcD4spnlYjxi9vXbSpwg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.20.0", "@typescript-eslint/types": "4.23.0",
"eslint-visitor-keys": "^2.0.0" "eslint-visitor-keys": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"eslint-visitor-keys": { "eslint-visitor-keys": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true "dev": true
} }
} }
@ -764,9 +671,9 @@
"integrity": "sha512-iiyRoho/ERzBUv6kFvfsrLNgTlVwOkqQcSQN7WrO3Y+c5SeuEhCn6+y1KwhM0V3ndptF5mI/RI44zkw0qcR5Jg==" "integrity": "sha512-iiyRoho/ERzBUv6kFvfsrLNgTlVwOkqQcSQN7WrO3Y+c5SeuEhCn6+y1KwhM0V3ndptF5mI/RI44zkw0qcR5Jg=="
}, },
"bufferutil": { "bufferutil": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.2.tgz", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz",
"integrity": "sha512-AtnG3W6M8B2n4xDQ5R+70EXvOpnXsFYg/AK2yTZd+HQ/oxAdz+GI+DvjmhBw3L0ole+LJ0ngqY4JMbDzkfNzhA==", "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==",
"optional": true, "optional": true,
"requires": { "requires": {
"node-gyp-build": "^4.2.0" "node-gyp-build": "^4.2.0"
@ -778,16 +685,6 @@
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
"dev": true "dev": true
}, },
"call-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz",
"integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.0"
}
},
"callsites": { "callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -795,14 +692,54 @@
"dev": true "dev": true
}, },
"chalk": { "chalk": {
"version": "2.4.2", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-styles": "^3.2.1", "ansi-styles": "^4.1.0",
"escape-string-regexp": "^1.0.5", "supports-color": "^7.1.0"
"supports-color": "^5.3.0" },
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
} }
}, },
"check-more-types": { "check-more-types": {
@ -953,9 +890,10 @@
} }
}, },
"dotenv": { "dotenv": {
"version": "8.2.0", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==",
"dev": true
}, },
"duplexer": { "duplexer": {
"version": "0.1.2", "version": "0.1.2",
@ -1028,13 +966,13 @@
"dev": true "dev": true
}, },
"eslint": { "eslint": {
"version": "7.23.0", "version": "7.26.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.23.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.26.0.tgz",
"integrity": "sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==", "integrity": "sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "7.12.11", "@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.0", "@eslint/eslintrc": "^0.4.1",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@ -1072,75 +1010,11 @@
"v8-compile-cache": "^2.0.3" "v8-compile-cache": "^2.0.3"
}, },
"dependencies": { "dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"eslint-visitor-keys": { "eslint-visitor-keys": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true "dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
} }
} }
}, },
@ -1151,9 +1025,9 @@
"dev": true "dev": true
}, },
"eslint-plugin-prettier": { "eslint-plugin-prettier": {
"version": "3.3.1", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz",
"integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==", "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==",
"dev": true, "dev": true,
"requires": { "requires": {
"prettier-linter-helpers": "^1.0.0" "prettier-linter-helpers": "^1.0.0"
@ -1446,17 +1320,6 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true "dev": true
}, },
"get-intrinsic": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz",
"integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
}
},
"get-prototype-of": { "get-prototype-of": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/get-prototype-of/-/get-prototype-of-0.0.0.tgz", "resolved": "https://registry.npmjs.org/get-prototype-of/-/get-prototype-of-0.0.0.tgz",
@ -1495,9 +1358,9 @@
} }
}, },
"globals": { "globals": {
"version": "13.7.0", "version": "13.8.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.7.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz",
"integrity": "sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA==", "integrity": "sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"type-fest": "^0.20.2" "type-fest": "^0.20.2"
@ -1554,12 +1417,6 @@
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true "dev": true
}, },
"has-symbols": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
"dev": true
},
"hash.js": { "hash.js": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
@ -1627,15 +1484,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"is-boolean-object": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz",
"integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==",
"dev": true,
"requires": {
"call-bind": "^1.0.0"
}
},
"is-capitalized": { "is-capitalized": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-capitalized/-/is-capitalized-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-capitalized/-/is-capitalized-1.0.0.tgz",
@ -1688,12 +1536,6 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true "dev": true
}, },
"is-number-object": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz",
"integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==",
"dev": true
},
"is-reference": { "is-reference": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@ -1709,12 +1551,6 @@
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
"dev": true "dev": true
}, },
"is-string": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
"integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
"dev": true
},
"isexe": { "isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -1722,9 +1558,9 @@
"dev": true "dev": true
}, },
"jayson": { "jayson": {
"version": "3.4.4", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-3.4.4.tgz", "resolved": "https://registry.npmjs.org/jayson/-/jayson-3.6.2.tgz",
"integrity": "sha512-fgQflh+Qnhdv9fjxTnpTsa2WUG/dgyeKQzIh5MJ77Qv2sqFyyAZn7mTUYgPjJMFjsKfb4HNsSBh6ktJeeQiAGQ==", "integrity": "sha512-hbl+x2xH6FT7nckw+Pq3lKOIJaMBKOgNJEVfvloDBWB8iSfzn/1U2igj1A5rplqNMFN/OnnaTNw8qPKVmoq83Q==",
"requires": { "requires": {
"@types/connect": "^3.4.33", "@types/connect": "^3.4.33",
"@types/express-serve-static-core": "^4.17.9", "@types/express-serve-static-core": "^4.17.9",
@ -1740,9 +1576,9 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "12.20.7", "version": "12.20.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.7.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.12.tgz",
"integrity": "sha512-gWL8VUkg8VRaCAUgG9WmhefMqHmMblxe2rVpMF86nZY/+ZysU+BkAp+3cz03AixWDSSz0ks5WX59yAhv/cDwFA==" "integrity": "sha512-KQZ1al2hKOONAs2MFv+yTQP1LkDWMrRJ9YCVRalXltOfXsBmH5IownLxQaiq0lnAHwAViLnh2aTYqrPcRGEbgg=="
} }
} }
}, },
@ -1845,9 +1681,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.20", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"lodash.clonedeep": { "lodash.clonedeep": {
"version": "4.5.0", "version": "4.5.0",
@ -1855,12 +1691,6 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true "dev": true
}, },
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
"dev": true
},
"lodash.truncate": { "lodash.truncate": {
"version": "4.4.2", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
@ -2227,9 +2057,9 @@
} }
}, },
"rollup": { "rollup": {
"version": "2.44.0", "version": "2.47.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.44.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.47.0.tgz",
"integrity": "sha512-rGSF4pLwvuaH/x4nAS+zP6UNn5YUDWf/TeEU5IoXSZKBbKRNTCI3qMnYXKZgrC0D2KzS2baiOZt1OlqhMu5rnQ==", "integrity": "sha512-rqBjgq9hQfW0vRmz+0S062ORRNJXvwRpzxhFXORvar/maZqY6za3rgQ/p1Glg+j1hnc1GtYyQCPiAei95uTElg==",
"dev": true, "dev": true,
"requires": { "requires": {
"fsevents": "~2.3.1" "fsevents": "~2.3.1"
@ -2332,9 +2162,9 @@
} }
}, },
"rpc-websockets": { "rpc-websockets": {
"version": "7.4.6", "version": "7.4.11",
"resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.4.6.tgz", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.4.11.tgz",
"integrity": "sha512-vDGdyJv858O5ZIc7glov8pQDdFztOqujA7iNyrfPxw87ajHT5s8WQU4MLNEG8pTR/xzqOn06dYH7kef2hijInw==", "integrity": "sha512-/6yKCkRrEEb+TlJb6Q/pNBD4WdO/tFxE22rQYBl1YyIgz3SpzQDQ/0qAMWWksjFkDayiq3xVxmkP8e/tL422ZA==",
"requires": { "requires": {
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"assert-args": "^1.2.1", "assert-args": "^1.2.1",
@ -2347,9 +2177,9 @@
}, },
"dependencies": { "dependencies": {
"uuid": { "uuid": {
"version": "8.3.1", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
} }
} }
}, },
@ -2383,6 +2213,15 @@
"node-gyp-build": "^4.2.0" "node-gyp-build": "^4.2.0"
} }
}, },
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"shebang-command": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2576,26 +2415,23 @@
} }
}, },
"table": { "table": {
"version": "6.0.9", "version": "6.7.0",
"resolved": "https://registry.npmjs.org/table/-/table-6.0.9.tgz", "resolved": "https://registry.npmjs.org/table/-/table-6.7.0.tgz",
"integrity": "sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==", "integrity": "sha512-SAM+5p6V99gYiiy2gT5ArdzgM1dLDed0nkrWmG6Fry/bUS/m9x83BwpJUOf1Qj/x2qJd+thL6IkIx7qPGRxqBw==",
"dev": true, "dev": true,
"requires": { "requires": {
"ajv": "^8.0.1", "ajv": "^8.0.1",
"is-boolean-object": "^1.1.0",
"is-number-object": "^1.0.4",
"is-string": "^1.0.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.truncate": "^4.4.2", "lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0", "slice-ansi": "^4.0.0",
"string-width": "^4.2.0" "string-width": "^4.2.0",
"strip-ansi": "^6.0.0"
}, },
"dependencies": { "dependencies": {
"ajv": { "ajv": {
"version": "8.0.1", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.0.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.3.0.tgz",
"integrity": "sha512-46ZA4TalFcLLqX1dEU3dhdY38wAtDydJ4e7QQTVekLUTzXkb1LfqU6VOBXC/a9wiv4T094WURqJH6ZitF92Kqw==", "integrity": "sha512-RYE7B5An83d7eWnDR8kbdaIFqmKCNsP16ay1hDbJEU+sa0e3H9SebskCt0Uufem6cfAVu7Col6ubcn/W+Sm8/Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
@ -2687,9 +2523,9 @@
"dev": true "dev": true
}, },
"typescript": { "typescript": {
"version": "4.2.3", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
"dev": true "dev": true
}, },
"universalify": { "universalify": {
@ -2708,9 +2544,9 @@
} }
}, },
"utf-8-validate": { "utf-8-validate": {
"version": "5.0.3", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.3.tgz", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz",
"integrity": "sha512-jtJM6fpGv8C1SoH4PtG22pGto6x+Y8uPprW0tw3//gGFhDDTiuksgradgFN6yRayDP4SyZZa6ZMGHLIa17+M8A==", "integrity": "sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ==",
"optional": true, "optional": true,
"requires": { "requires": {
"node-gyp-build": "^4.2.0" "node-gyp-build": "^4.2.0"
@ -2738,14 +2574,6 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"rxjs": "^6.6.3" "rxjs": "^6.6.3"
},
"dependencies": {
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
}
} }
}, },
"word-wrap": { "word-wrap": {
@ -2761,9 +2589,9 @@
"dev": true "dev": true
}, },
"ws": { "ws": {
"version": "7.4.0", "version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==" "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g=="
}, },
"yallist": { "yallist": {
"version": "4.0.0", "version": "4.0.0",

View File

@ -34,37 +34,37 @@
"cluster:mainnet-beta": "cp cluster-mainnet-beta.env .env" "cluster:mainnet-beta": "cp cluster-mainnet-beta.env .env"
}, },
"dependencies": { "dependencies": {
"@solana/spl-token": "0.1.3", "@solana/spl-token": "0.1.4",
"@solana/web3.js": "^1.2.5", "@solana/web3.js": "^1.10.1",
"bn.js": "^5.2.0", "bn.js": "^5.2.0",
"mkdirp": "^1.0.4" "mkdirp": "^1.0.4"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^18.0.0", "@rollup/plugin-commonjs": "^19.0.0",
"@tsconfig/recommended": "^1.0.1", "@tsconfig/recommended": "^1.0.1",
"@types/bn.js": "^5.1.0", "@types/bn.js": "^5.1.0",
"@types/eslint": "^7.2.8", "@types/eslint": "^7.2.10",
"@types/eslint-plugin-prettier": "^3.1.0", "@types/eslint-plugin-prettier": "^3.1.0",
"@types/mkdirp": "^1.0.1", "@types/mkdirp": "^1.0.1",
"@types/mz": "^2.7.3", "@types/mz": "^2.7.3",
"@types/node": "^14.14.37", "@types/node": "^15.0.2",
"@types/prettier": "^2.2.3", "@types/prettier": "^2.2.3",
"@types/rollup-plugin-json": "^3.0.2", "@types/rollup-plugin-json": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^4.20.0", "@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.20.0", "@typescript-eslint/parser": "^4.23.0",
"dotenv": "^8.2.0", "dotenv": "^9.0.2",
"eslint": "^7.23.0", "eslint": "^7.26.0",
"eslint-config-prettier": "^7.2.0", "eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.4.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"rollup": "^2.44.0", "rollup": "^2.47.0",
"rollup-plugin-json": "^4.0.0", "rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-sourcemaps": "^0.6.3", "rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.30.0", "rollup-plugin-typescript2": "^0.30.0",
"start-server-and-test": "^1.11.6", "start-server-and-test": "^1.11.6",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",
"typescript": "^4.2.3" "typescript": "^4.2.4"
}, },
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"

View File

@ -14,10 +14,10 @@ test-dump-genesis-accounts = []
[dependencies] [dependencies]
arrayref = "0.3.6" arrayref = "0.3.6"
flux-aggregator = { git = "https://github.com/octopus-network/solana-flux-aggregator", rev = "9cfaec5", features = ["no-entrypoint"] }
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "991a86e", features = ["no-entrypoint"] } solana-program = "1.6.7"
solana-program = "1.6.2"
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] } spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }
thiserror = "1.0" thiserror = "1.0"
uint = "0.8" uint = "0.8"
@ -27,8 +27,8 @@ assert_matches = "1.5.0"
base64 = "0.13" base64 = "0.13"
log = "0.4.14" log = "0.4.14"
proptest = "0.10" proptest = "0.10"
solana-program-test = "1.6.2" solana-program-test = "1.6.7"
solana-sdk = "1.6.2" solana-sdk = "1.6.7"
serde = "1.0" serde = "1.0"
serde_yaml = "0.8" serde_yaml = "0.8"

View File

@ -1,345 +0,0 @@
//! Dex market used for simulating trades
use crate::{
error::LendingError,
math::{Decimal, TryAdd, TryDiv, TryMul, TrySub},
state::TokenConverter,
};
use arrayref::{array_refs, mut_array_refs};
use serum_dex::critbit::{Slab, SlabView};
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
use std::{cell::RefMut, convert::TryFrom};
/// Side of the dex market order book
#[derive(Clone, Copy, PartialEq)]
enum Side {
Bid,
Ask,
}
/// Market currency
#[derive(Clone, Copy, PartialEq)]
enum Currency {
Base,
Quote,
}
impl Currency {
fn opposite(&self) -> Self {
match self {
Currency::Base => Currency::Quote,
Currency::Quote => Currency::Base,
}
}
}
/// Trade action for trade simulator
#[derive(PartialEq)]
pub enum TradeAction {
/// Sell tokens
Sell,
/// Buy tokens
Buy,
}
/// Dex market order
struct Order {
price: u64,
quantity: u64,
}
/// Trade simulator
pub struct TradeSimulator<'a> {
dex_market: DexMarket,
orders: DexMarketOrders<'a>,
orders_side: Side,
quote_token_mint: &'a Pubkey,
buy_token_mint: &'a Pubkey,
sell_token_mint: &'a Pubkey,
}
impl<'a> TokenConverter for TradeSimulator<'a> {
fn best_price(&mut self, token_mint: &Pubkey) -> Result<Decimal, ProgramError> {
let action = if token_mint == self.buy_token_mint {
TradeAction::Buy
} else {
TradeAction::Sell
};
let currency = if token_mint == self.quote_token_mint {
Currency::Quote
} else {
Currency::Base
};
let order_book_side = match (action, currency) {
(TradeAction::Buy, Currency::Base) => Side::Ask,
(TradeAction::Sell, Currency::Quote) => Side::Ask,
(TradeAction::Buy, Currency::Quote) => Side::Bid,
(TradeAction::Sell, Currency::Base) => Side::Bid,
};
if order_book_side != self.orders_side {
return Err(LendingError::DexInvalidOrderBookSide.into());
}
let best_order_price = self
.orders
.best_order_price()
.ok_or(LendingError::TradeSimulationError)?;
let input_token = Decimal::one().try_div(self.dex_market.get_lots(currency))?;
let output_token_price = if currency == Currency::Base {
input_token.try_mul(best_order_price)
} else {
input_token.try_div(best_order_price)
}?;
output_token_price.try_mul(self.dex_market.get_lots(currency.opposite()))
}
fn convert(
self,
from_amount: Decimal,
from_token_mint: &Pubkey,
) -> Result<Decimal, ProgramError> {
let action = if from_token_mint == self.buy_token_mint {
TradeAction::Buy
} else {
TradeAction::Sell
};
self.simulate_trade(action, from_amount)
}
}
impl<'a> TradeSimulator<'a> {
/// Create a new TradeSimulator
pub fn new(
dex_market_info: &AccountInfo,
dex_market_orders: &AccountInfo,
memory: &'a AccountInfo,
quote_token_mint: &'a Pubkey,
buy_token_mint: &'a Pubkey,
sell_token_mint: &'a Pubkey,
) -> Result<Self, ProgramError> {
let dex_market = DexMarket::new(dex_market_info);
let orders = DexMarketOrders::new(&dex_market, dex_market_orders, memory)?;
let orders_side = orders.side;
Ok(Self {
dex_market,
orders,
orders_side,
quote_token_mint,
buy_token_mint,
sell_token_mint,
})
}
/// Simulate a trade
pub fn simulate_trade(
mut self,
action: TradeAction,
quantity: Decimal,
) -> Result<Decimal, ProgramError> {
let token_mint = match action {
TradeAction::Buy => self.buy_token_mint,
TradeAction::Sell => self.sell_token_mint,
};
let currency = if token_mint == self.quote_token_mint {
Currency::Quote
} else {
Currency::Base
};
let order_book_side = match (action, currency) {
(TradeAction::Buy, Currency::Base) => Side::Ask,
(TradeAction::Sell, Currency::Quote) => Side::Ask,
(TradeAction::Buy, Currency::Quote) => Side::Bid,
(TradeAction::Sell, Currency::Base) => Side::Bid,
};
if order_book_side != self.orders_side {
return Err(LendingError::DexInvalidOrderBookSide.into());
}
let input_quantity: Decimal = quantity.try_div(self.dex_market.get_lots(currency))?;
let output_quantity = self.exchange_with_order_book(input_quantity, currency)?;
output_quantity.try_mul(self.dex_market.get_lots(currency.opposite()))
}
/// Exchange tokens by filling orders
fn exchange_with_order_book(
&mut self,
mut input_quantity: Decimal,
currency: Currency,
) -> Result<Decimal, ProgramError> {
let mut output_quantity = Decimal::zero();
let zero = Decimal::zero();
while input_quantity > zero {
let next_order = self
.orders
.next()
.ok_or_else(|| ProgramError::from(LendingError::TradeSimulationError))?;
let next_order_price = next_order.price;
let base_quantity = next_order.quantity;
let (filled, output) = if currency == Currency::Base {
let filled = input_quantity.min(Decimal::from(base_quantity));
(filled, filled.try_mul(next_order_price)?)
} else {
let quote_quantity = Decimal::from(base_quantity).try_mul(next_order_price)?;
let filled = input_quantity.min(quote_quantity);
(filled, filled.try_div(next_order_price)?)
};
input_quantity = input_quantity.try_sub(filled)?;
output_quantity = output_quantity.try_add(output)?;
}
Ok(output_quantity)
}
}
/// Dex market order account info
struct DexMarketOrders<'a> {
heap: Option<RefMut<'a, Slab>>,
side: Side,
}
impl<'a> DexMarketOrders<'a> {
/// Create a new DexMarketOrders
fn new(
dex_market: &DexMarket,
orders: &AccountInfo,
memory: &'a AccountInfo,
) -> Result<Self, ProgramError> {
let side = match orders.key {
key if key == &dex_market.bids => Side::Bid,
key if key == &dex_market.asks => Side::Ask,
_ => return Err(LendingError::DexInvalidOrderBookSide.into()),
};
if memory.data_len() < orders.data_len() {
return Err(LendingError::MemoryTooSmall.into());
}
let mut memory_data = memory.data.borrow_mut();
fast_copy(&orders.data.borrow(), &mut memory_data);
let heap = Some(RefMut::map(memory_data, |bytes| {
// strip padding and header
let start = 5 + 8;
let end = bytes.len() - 7;
Slab::new(&mut bytes[start..end])
}));
Ok(Self { heap, side })
}
fn best_order_price(&mut self) -> Option<u64> {
let side = self.side;
self.heap.as_mut().and_then(|heap| {
let handle = match side {
Side::Bid => heap.find_max(),
Side::Ask => heap.find_min(),
}?;
Some(heap.get_mut(handle)?.as_leaf_mut()?.price().get())
})
}
}
impl Iterator for DexMarketOrders<'_> {
type Item = Order;
fn next(&mut self) -> Option<Order> {
let leaf_node = match self.side {
Side::Bid => self.heap.as_mut().and_then(|heap| heap.remove_max()),
Side::Ask => self.heap.as_mut().and_then(|heap| heap.remove_min()),
}?;
Some(Order {
price: leaf_node.price().get(),
quantity: leaf_node.quantity(),
})
}
}
/// Offset for dex market base mint
pub const BASE_MINT_OFFSET: usize = 6;
/// Offset for dex market quote mint
pub const QUOTE_MINT_OFFSET: usize = 10;
const BIDS_OFFSET: usize = 35;
const ASKS_OFFSET: usize = 39;
/// Dex market info
pub struct DexMarket {
bids: Pubkey,
asks: Pubkey,
base_lots: u64,
quote_lots: u64,
}
impl DexMarket {
/// Create a new DexMarket
fn new(dex_market_info: &AccountInfo) -> Self {
let dex_market_data = dex_market_info.data.borrow();
let bids = Self::pubkey_at_offset(&dex_market_data, BIDS_OFFSET);
let asks = Self::pubkey_at_offset(&dex_market_data, ASKS_OFFSET);
let base_lots = Self::base_lots(&dex_market_data);
let quote_lots = Self::quote_lots(&dex_market_data);
Self {
bids,
asks,
base_lots,
quote_lots,
}
}
fn get_lots(&self, currency: Currency) -> u64 {
match currency {
Currency::Base => self.base_lots,
Currency::Quote => self.quote_lots,
}
}
fn base_lots(data: &[u8]) -> u64 {
let count_start = 5 + 43 * 8;
let count_end = count_start + 8;
u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap())
}
fn quote_lots(data: &[u8]) -> u64 {
let count_start = 5 + 44 * 8;
let count_end = count_start + 8;
u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap())
}
/// Get pubkey located at offset
pub fn pubkey_at_offset(data: &[u8], offset: usize) -> Pubkey {
let count_start = 5 + offset * 8;
let count_end = count_start + 32;
Pubkey::new(&data[count_start..count_end])
}
}
/// A more efficient `copy_from_slice` implementation.
fn fast_copy(mut src: &[u8], mut dst: &mut [u8]) {
const COPY_SIZE: usize = 512;
while src.len() >= COPY_SIZE {
#[allow(clippy::ptr_offset_with_cast)]
let (src_word, src_rem) = array_refs![src, COPY_SIZE; ..;];
#[allow(clippy::ptr_offset_with_cast)]
let (dst_word, dst_rem) = mut_array_refs![dst, COPY_SIZE; ..;];
*dst_word = *src_word;
src = src_rem;
dst = dst_rem;
}
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), src.len());
}
}

View File

@ -31,17 +31,20 @@ pub enum LendingError {
/// The owner of the account input isn't set to the correct token program id. /// The owner of the account input isn't set to the correct token program id.
#[error("Input token account is not owned by the correct token program id")] #[error("Input token account is not owned by the correct token program id")]
InvalidTokenOwner, InvalidTokenOwner,
/// Expected an SPL Token account
#[error("Input token account is not valid")]
InvalidTokenAccount,
/// Expected an SPL Token mint /// Expected an SPL Token mint
#[error("Input token mint account is not valid")] #[error("Input token mint account is not valid")]
InvalidTokenMint, InvalidTokenMint,
/// Expected a different SPL Token program /// Expected a different SPL Token program
#[error("Input token program account is not valid")] #[error("Input token program account is not valid")]
InvalidTokenProgram, InvalidTokenProgram,
// 10
/// Invalid amount, must be greater than zero /// Invalid amount, must be greater than zero
#[error("Input amount is invalid")] #[error("Input amount is invalid")]
InvalidAmount, InvalidAmount,
// 10
/// Invalid config value /// Invalid config value
#[error("Input config value is invalid")] #[error("Input config value is invalid")]
InvalidConfig, InvalidConfig,
@ -54,62 +57,8 @@ pub enum LendingError {
/// Math operation overflow /// Math operation overflow
#[error("Math operation overflow")] #[error("Math operation overflow")]
MathOverflow, MathOverflow,
/// Negative interest rate
#[error("Interest rate is negative")]
NegativeInterestRate,
// 15 // 15
/// Memory is too small
#[error("Memory is too small")]
MemoryTooSmall,
/// The reserve lending market must be the same
#[error("Reserve mints do not match dex market mints")]
DexMarketMintMismatch,
/// The reserve lending market must be the same
#[error("Reserve lending market mismatch")]
LendingMarketMismatch,
/// The obligation token owner must be the same if reusing an obligation
#[error("Obligation token owner mismatch")]
ObligationTokenOwnerMismatch,
/// Insufficient liquidity available
#[error("Insufficient liquidity available")]
InsufficientLiquidity,
// 20
/// This reserve's collateral cannot be used for borrows
#[error("Input reserve has collateral disabled")]
ReserveCollateralDisabled,
/// Input reserves cannot be the same
#[error("Input reserves cannot be the same")]
DuplicateReserve,
/// Input reserves cannot use the same liquidity mint
#[error("Input reserves cannot use the same liquidity mint")]
DuplicateReserveMint,
/// Obligation amount is empty
#[error("Obligation amount is empty")]
ObligationEmpty,
/// Cannot liquidate healthy obligations
#[error("Cannot liquidate healthy obligations")]
HealthyObligation,
// 25
/// Borrow amount too small
#[error("Borrow amount too small")]
BorrowTooSmall,
/// Liquidation amount too small
#[error("Liquidation amount too small to receive collateral")]
LiquidationTooSmall,
/// Reserve state stale
#[error("Reserve state needs to be updated for the current slot")]
ReserveStale,
/// Trade simulation error
#[error("Trade simulation error")]
TradeSimulationError,
/// Invalid dex order book side
#[error("Invalid dex order book side")]
DexInvalidOrderBookSide,
// 30
/// Token initialize mint failed /// Token initialize mint failed
#[error("Token initialize mint failed")] #[error("Token initialize mint failed")]
TokenInitializeMintFailed, TokenInitializeMintFailed,
@ -126,16 +75,87 @@ pub enum LendingError {
#[error("Token burn failed")] #[error("Token burn failed")]
TokenBurnFailed, TokenBurnFailed,
// 20
/// Insufficient liquidity available
#[error("Insufficient liquidity available")]
InsufficientLiquidity,
/// This reserve's collateral cannot be used for borrows
#[error("Input reserve has collateral disabled")]
ReserveCollateralDisabled,
/// Reserve state stale
#[error("Reserve state needs to be refreshed")]
ReserveStale,
/// Withdraw amount too small
#[error("Withdraw amount too small")]
WithdrawTooSmall,
/// Withdraw amount too large
#[error("Withdraw amount too large")]
WithdrawTooLarge,
// 25
/// Borrow amount too small
#[error("Borrow amount too small to receive liquidity after fees")]
BorrowTooSmall,
/// Borrow amount too large
#[error("Borrow amount too large for deposited collateral")]
BorrowTooLarge,
/// Repay amount too small
#[error("Repay amount too small to transfer liquidity")]
RepayTooSmall,
/// Liquidation amount too small
#[error("Liquidation amount too small to receive collateral")]
LiquidationTooSmall,
/// Cannot liquidate healthy obligations
#[error("Cannot liquidate healthy obligations")]
ObligationHealthy,
// 30
/// Obligation state stale
#[error("Obligation state needs to be refreshed")]
ObligationStale,
/// Obligation reserve limit exceeded
#[error("Obligation reserve limit exceeded")]
ObligationReserveLimit,
/// Expected a different obligation owner
#[error("Obligation owner is invalid")]
InvalidObligationOwner,
/// Obligation deposits are empty
#[error("Obligation deposits are empty")]
ObligationDepositsEmpty,
/// Obligation borrows are empty
#[error("Obligation borrows are empty")]
ObligationBorrowsEmpty,
// 35 // 35
/// Invalid obligation collateral amount /// Obligation deposits have zero value
#[error("Invalid obligation collateral amount")] #[error("Obligation deposits have zero value")]
ObligationDepositsZero,
/// Obligation borrows have zero value
#[error("Obligation borrows have zero value")]
ObligationBorrowsZero,
/// Invalid obligation collateral
#[error("Invalid obligation collateral")]
InvalidObligationCollateral, InvalidObligationCollateral,
/// Obligation collateral is already below required amount /// Invalid obligation liquidity
#[error("Obligation collateral is already below required amount")] #[error("Invalid obligation liquidity")]
ObligationCollateralBelowRequired, InvalidObligationLiquidity,
/// Obligation collateral cannot be withdrawn below required amount /// Obligation collateral is empty
#[error("Obligation collateral cannot be withdrawn below required amount")] #[error("Obligation collateral is empty")]
ObligationCollateralWithdrawBelowRequired, ObligationCollateralEmpty,
// 40
/// Obligation liquidity is empty
#[error("Obligation liquidity is empty")]
ObligationLiquidityEmpty,
/// Negative interest rate
#[error("Interest rate is negative")]
NegativeInterestRate,
/// Oracle config is invalid
#[error("Input oracle config is invalid")]
InvalidOracleConfig,
/// Not enough liquidity after flash loan
#[error("Not enough liquidity after flash loan")]
NotEnoughLiquidityAfterFlashLoan,
} }
impl From<LendingError> for ProgramError { impl From<LendingError> for ProgramError {

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@
//! A lending program for the Solana blockchain. //! A lending program for the Solana blockchain.
pub mod dex_market;
pub mod entrypoint; pub mod entrypoint;
pub mod error; pub mod error;
pub mod instruction; pub mod instruction;

View File

@ -12,11 +12,12 @@
#![allow(clippy::ptr_offset_with_cast)] #![allow(clippy::ptr_offset_with_cast)]
#![allow(clippy::manual_range_contains)] #![allow(clippy::manual_range_contains)]
use crate::math::Rate; use crate::{
use crate::{error::LendingError, math::common::*}; error::LendingError,
math::{common::*, Rate},
};
use solana_program::program_error::ProgramError; use solana_program::program_error::ProgramError;
use std::convert::TryFrom; use std::{convert::TryFrom, fmt};
use std::fmt;
use uint::construct_uint; use uint::construct_uint;
// U192 with 192 bits consisting of 3 x 64-bit words // U192 with 192 bits consisting of 3 x 64-bit words
@ -55,6 +56,7 @@ impl Decimal {
} }
/// Return raw scaled value if it fits within u128 /// Return raw scaled value if it fits within u128
#[allow(clippy::wrong_self_convention)]
pub fn to_scaled_val(&self) -> Result<u128, ProgramError> { pub fn to_scaled_val(&self) -> Result<u128, ProgramError> {
Ok(u128::try_from(self.0).map_err(|_| LendingError::MathOverflow)?) Ok(u128::try_from(self.0).map_err(|_| LendingError::MathOverflow)?)
} }

View File

@ -62,6 +62,7 @@ impl Rate {
} }
/// Return raw scaled value /// Return raw scaled value
#[allow(clippy::wrong_self_convention)]
pub fn to_scaled_val(&self) -> u128 { pub fn to_scaled_val(&self) -> u128 {
self.0.as_u128() self.0.as_u128()
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
use crate::error::LendingError;
use solana_program::{clock::Slot, program_error::ProgramError};
use std::cmp::Ordering;
/// Number of slots to consider stale after
pub const STALE_AFTER_SLOTS_ELAPSED: u64 = 1;
/// Last update state
#[derive(Clone, Debug, Default)]
pub struct LastUpdate {
/// Last slot when updated
pub slot: Slot,
/// True when marked stale, false when slot updated
pub stale: bool,
}
impl LastUpdate {
/// Create new last update
pub fn new(slot: Slot) -> Self {
Self { slot, stale: true }
}
/// Return slots elapsed since given slot
pub fn slots_elapsed(&self, slot: Slot) -> Result<u64, ProgramError> {
let slots_elapsed = slot
.checked_sub(self.slot)
.ok_or(LendingError::MathOverflow)?;
Ok(slots_elapsed)
}
/// Set last update slot
pub fn update_slot(&mut self, slot: Slot) {
self.slot = slot;
self.stale = false;
}
/// Set stale to true
pub fn mark_stale(&mut self) {
self.stale = true;
}
/// Check if marked stale or last update slot is too long ago
pub fn is_stale(&self, slot: Slot) -> Result<bool, ProgramError> {
Ok(self.stale || self.slots_elapsed(slot)? >= STALE_AFTER_SLOTS_ELAPSED)
}
}
impl PartialEq for LastUpdate {
fn eq(&self, other: &Self) -> bool {
self.slot == other.slot
}
}
impl PartialOrd for LastUpdate {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.slot.partial_cmp(&other.slot)
}
}

View File

@ -1,9 +1,10 @@
use super::*; use super::*;
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
use solana_program::{ use solana_program::{
msg,
program_error::ProgramError, program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed}, program_pack::{IsInitialized, Pack, Sealed},
pubkey::Pubkey, pubkey::{Pubkey, PUBKEY_BYTES},
}; };
/// Lending market state /// Lending market state
@ -21,6 +22,36 @@ pub struct LendingMarket {
pub token_program_id: Pubkey, pub token_program_id: Pubkey,
} }
impl LendingMarket {
/// Create a new lending market
pub fn new(params: InitLendingMarketParams) -> Self {
let mut lending_market = Self::default();
Self::init(&mut lending_market, params);
lending_market
}
/// Initialize a lending market
pub fn init(&mut self, params: InitLendingMarketParams) {
self.version = PROGRAM_VERSION;
self.bump_seed = params.bump_seed;
self.token_program_id = params.token_program_id;
self.quote_token_mint = params.quote_token_mint;
self.owner = params.owner;
}
}
/// Initialize a lending market
pub struct InitLendingMarketParams {
/// Bump seed for derived authority address
pub bump_seed: u8,
/// Owner authority which can add new reserves
pub owner: Pubkey,
/// Quote currency token mint
pub quote_token_mint: Pubkey,
/// Token program id
pub token_program_id: Pubkey,
}
impl Sealed for LendingMarket {} impl Sealed for LendingMarket {}
impl IsInitialized for LendingMarket { impl IsInitialized for LendingMarket {
fn is_initialized(&self) -> bool { fn is_initialized(&self) -> bool {
@ -28,18 +59,33 @@ impl IsInitialized for LendingMarket {
} }
} }
const LENDING_MARKET_LEN: usize = 160; const LENDING_MARKET_LEN: usize = 226; // 1 + 1 + 32 + 32 + 32 + 128
impl Pack for LendingMarket { impl Pack for LendingMarket {
const LEN: usize = 160; const LEN: usize = LENDING_MARKET_LEN;
/// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html). fn pack_into_slice(&self, output: &mut [u8]) {
let output = array_mut_ref![output, 0, LENDING_MARKET_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (version, bump_seed, owner, quote_token_mint, token_program_id, _padding) =
mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128];
*version = self.version.to_le_bytes();
*bump_seed = self.bump_seed.to_le_bytes();
owner.copy_from_slice(self.owner.as_ref());
quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref());
token_program_id.copy_from_slice(self.token_program_id.as_ref());
}
/// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html)
fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> { fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
let input = array_ref![input, 0, LENDING_MARKET_LEN]; let input = array_ref![input, 0, LENDING_MARKET_LEN];
#[allow(clippy::ptr_offset_with_cast)] #[allow(clippy::ptr_offset_with_cast)]
let (version, bump_seed, owner, quote_token_mint, token_program_id, _padding) = let (version, bump_seed, owner, quote_token_mint, token_program_id, _padding) =
array_refs![input, 1, 1, 32, 32, 32, 62]; array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128];
let version = u8::from_le_bytes(*version); let version = u8::from_le_bytes(*version);
if version > PROGRAM_VERSION { if version > PROGRAM_VERSION {
msg!("Lending market version does not match lending program version");
return Err(ProgramError::InvalidAccountData); return Err(ProgramError::InvalidAccountData);
} }
@ -51,16 +97,4 @@ impl Pack for LendingMarket {
token_program_id: Pubkey::new_from_array(*token_program_id), token_program_id: Pubkey::new_from_array(*token_program_id),
}) })
} }
fn pack_into_slice(&self, output: &mut [u8]) {
let output = array_mut_ref![output, 0, LENDING_MARKET_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (version, bump_seed, owner, quote_token_mint, token_program_id, _padding) =
mut_array_refs![output, 1, 1, 32, 32, 32, 62];
*version = self.version.to_le_bytes();
*bump_seed = self.bump_seed.to_le_bytes();
owner.copy_from_slice(self.owner.as_ref());
quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref());
token_program_id.copy_from_slice(self.token_program_id.as_ref());
}
} }

View File

@ -1,9 +1,11 @@
//! State types //! State types
mod last_update;
mod lending_market; mod lending_market;
mod obligation; mod obligation;
mod reserve; mod reserve;
pub use last_update::*;
pub use lending_market::*; pub use lending_market::*;
pub use obligation::*; pub use obligation::*;
pub use reserve::*; pub use reserve::*;
@ -12,13 +14,15 @@ use crate::math::{Decimal, WAD};
use arrayref::{array_refs, mut_array_refs}; use arrayref::{array_refs, mut_array_refs};
use solana_program::{ use solana_program::{
clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY}, clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY},
msg,
program_error::ProgramError, program_error::ProgramError,
program_option::COption, program_option::COption,
pubkey::Pubkey, pubkey::{Pubkey, PUBKEY_BYTES},
}; };
/// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity) /// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity)
pub const INITIAL_COLLATERAL_RATIO: u64 = 5; // @FIXME: restore to 5
pub const INITIAL_COLLATERAL_RATIO: u64 = 1;
const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD; const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD;
/// Current version of the program and all new accounts created /// Current version of the program and all new accounts created
@ -32,22 +36,10 @@ pub const UNINITIALIZED_VERSION: u8 = 0;
pub const SLOTS_PER_YEAR: u64 = pub const SLOTS_PER_YEAR: u64 =
DEFAULT_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT * SECONDS_PER_DAY * 365; DEFAULT_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT * SECONDS_PER_DAY * 365;
/// Token converter
pub trait TokenConverter {
/// Return best price for specified token
fn best_price(&mut self, token_mint: &Pubkey) -> Result<Decimal, ProgramError>;
/// Convert between two different tokens
fn convert(
self,
from_amount: Decimal,
from_token_mint: &Pubkey,
) -> Result<Decimal, ProgramError>;
}
// Helpers // Helpers
fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 36]) { fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 4 + PUBKEY_BYTES]) {
let (tag, body) = mut_array_refs![dst, 4, 32]; #[allow(clippy::ptr_offset_with_cast)]
let (tag, body) = mut_array_refs![dst, 4, PUBKEY_BYTES];
match src { match src {
COption::Some(key) => { COption::Some(key) => {
*tag = [1, 0, 0, 0]; *tag = [1, 0, 0, 0];
@ -59,19 +51,23 @@ fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 36]) {
} }
} }
fn unpack_coption_key(src: &[u8; 36]) -> Result<COption<Pubkey>, ProgramError> { fn unpack_coption_key(src: &[u8; 4 + PUBKEY_BYTES]) -> Result<COption<Pubkey>, ProgramError> {
let (tag, body) = array_refs![src, 4, 32]; #[allow(clippy::ptr_offset_with_cast)]
let (tag, body) = array_refs![src, 4, PUBKEY_BYTES];
match *tag { match *tag {
[0, 0, 0, 0] => Ok(COption::None), [0, 0, 0, 0] => Ok(COption::None),
[1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))),
_ => Err(ProgramError::InvalidAccountData), _ => {
msg!("COption<Pubkey> cannot be unpacked");
Err(ProgramError::InvalidAccountData)
}
} }
} }
fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) { fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) {
*dst = decimal *dst = decimal
.to_scaled_val() .to_scaled_val()
.expect("could not pack decimal") .expect("Decimal cannot be packed")
.to_le_bytes(); .to_le_bytes();
} }
@ -79,6 +75,21 @@ fn unpack_decimal(src: &[u8; 16]) -> Decimal {
Decimal::from_scaled_val(u128::from_le_bytes(*src)) Decimal::from_scaled_val(u128::from_le_bytes(*src))
} }
fn pack_bool(boolean: bool, dst: &mut [u8; 1]) {
*dst = (boolean as u8).to_le_bytes()
}
fn unpack_bool(src: &[u8; 1]) -> Result<bool, ProgramError> {
match u8::from_le_bytes(*src) {
0 => Ok(false),
1 => Ok(true),
_ => {
msg!("Boolean cannot be unpacked");
Err(ProgramError::InvalidAccountData)
}
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -1,185 +1,220 @@
use super::*; use super::*;
use crate::{ use crate::{
error::LendingError, error::LendingError,
math::{Decimal, Rate, TryDiv, TryMul, TrySub}, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub},
}; };
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
use solana_program::{ use solana_program::{
clock::Slot,
entrypoint::ProgramResult, entrypoint::ProgramResult,
msg,
program_error::ProgramError, program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed}, program_pack::{IsInitialized, Pack, Sealed},
pubkey::Pubkey, pubkey::{Pubkey, PUBKEY_BYTES},
};
use std::{
cmp::Ordering,
convert::{TryFrom, TryInto},
}; };
use std::convert::TryInto;
/// Borrow obligation state /// Max number of collateral and liquidity reserve accounts combined for an obligation
pub const MAX_OBLIGATION_RESERVES: usize = 10;
/// Lending market obligation state
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, Default, PartialEq)]
pub struct Obligation { pub struct Obligation {
/// Version of the obligation /// Version of the struct
pub version: u8, pub version: u8,
/// Amount of collateral tokens deposited for this obligation /// Last update to collateral, liquidity, or their market values
pub deposited_collateral_tokens: u64, pub last_update: LastUpdate,
/// Reserve which collateral tokens were deposited into /// Lending market address
pub collateral_reserve: Pubkey, pub lending_market: Pubkey,
/// Borrow rate used for calculating interest. /// Owner authority which can borrow liquidity
pub cumulative_borrow_rate_wads: Decimal, pub owner: Pubkey,
/// Amount of tokens borrowed for this obligation plus interest /// Deposited collateral for the obligation, unique by deposit reserve address
pub borrowed_liquidity_wads: Decimal, pub deposits: Vec<ObligationCollateral>,
/// Reserve which tokens were borrowed from /// Borrowed liquidity for the obligation, unique by borrow reserve address
pub borrow_reserve: Pubkey, pub borrows: Vec<ObligationLiquidity>,
/// Mint address of the tokens for this obligation /// Market value of deposits
pub token_mint: Pubkey, pub deposited_value: Decimal,
/// Market value of borrows
pub borrowed_value: Decimal,
/// The maximum borrow value at the weighted average loan to value ratio
pub allowed_borrow_value: Decimal,
/// The dangerous borrow value at the weighted average liquidation threshold
pub unhealthy_borrow_value: Decimal,
} }
impl Obligation { impl Obligation {
/// Create new obligation /// Create a new obligation
pub fn new(params: NewObligationParams) -> Self { pub fn new(params: InitObligationParams) -> Self {
let NewObligationParams { let mut obligation = Self::default();
collateral_reserve, Self::init(&mut obligation, params);
borrow_reserve, obligation
token_mint,
cumulative_borrow_rate_wads,
} = params;
Self {
version: PROGRAM_VERSION,
deposited_collateral_tokens: 0,
collateral_reserve,
cumulative_borrow_rate_wads,
borrowed_liquidity_wads: Decimal::zero(),
borrow_reserve,
token_mint,
}
} }
/// Maximum amount of loan that can be closed out by a liquidator due /// Initialize an obligation
/// to the remaining balance being too small to be liquidated normally. pub fn init(&mut self, params: InitObligationParams) {
pub fn max_closeable_amount(&self) -> Result<u64, ProgramError> { self.version = PROGRAM_VERSION;
if self.borrowed_liquidity_wads < Decimal::from(CLOSEABLE_AMOUNT) { self.last_update = LastUpdate::new(params.current_slot);
self.borrowed_liquidity_wads.try_ceil_u64() self.lending_market = params.lending_market;
self.owner = params.owner;
self.deposits = params.deposits;
self.borrows = params.borrows;
}
/// Calculate the current ratio of borrowed value to deposited value
pub fn loan_to_value(&self) -> Result<Decimal, ProgramError> {
self.borrowed_value.try_div(self.deposited_value)
}
/// Repay liquidity and remove it from borrows if zeroed out
pub fn repay(&mut self, settle_amount: Decimal, liquidity_index: usize) -> ProgramResult {
let liquidity = &mut self.borrows[liquidity_index];
if settle_amount == liquidity.borrowed_amount_wads {
self.borrows.remove(liquidity_index);
} else { } else {
Ok(0) liquidity.repay(settle_amount)?;
} }
Ok(())
} }
/// Maximum amount of loan that can be repaid by liquidators /// Withdraw collateral and remove it from deposits if zeroed out
pub fn max_liquidation_amount(&self) -> Result<u64, ProgramError> { pub fn withdraw(&mut self, withdraw_amount: u64, collateral_index: usize) -> ProgramResult {
self.borrowed_liquidity_wads let collateral = &mut self.deposits[collateral_index];
.try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? if withdraw_amount == collateral.deposited_amount {
.try_floor_u64() self.deposits.remove(collateral_index);
} else {
collateral.withdraw(withdraw_amount)?;
}
Ok(())
} }
/// Ratio of loan balance to collateral value /// Calculate the maximum collateral value that can be withdrawn
pub fn loan_to_value( pub fn max_withdraw_value(&self) -> Result<Decimal, ProgramError> {
let required_deposit_value = self
.borrowed_value
.try_mul(self.deposited_value)?
.try_div(self.allowed_borrow_value)?;
if required_deposit_value >= self.deposited_value {
return Ok(Decimal::zero());
}
self.deposited_value.try_sub(required_deposit_value)
}
/// Calculate the maximum liquidity value that can be borrowed
pub fn remaining_borrow_value(&self) -> Result<Decimal, ProgramError> {
self.allowed_borrow_value.try_sub(self.borrowed_value)
}
/// Calculate the maximum liquidation amount for a given liquidity
pub fn max_liquidation_amount(
&self, &self,
collateral_exchange_rate: CollateralExchangeRate, liquidity: &ObligationLiquidity,
borrow_token_price: Decimal,
) -> Result<Decimal, ProgramError> { ) -> Result<Decimal, ProgramError> {
let loan = self.borrowed_liquidity_wads; let max_liquidation_value = self
let collateral_value = collateral_exchange_rate .borrowed_value
.decimal_collateral_to_liquidity(self.deposited_collateral_tokens.into())? .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
.try_div(borrow_token_price)?; .min(liquidity.market_value);
loan.try_div(collateral_value) let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?;
liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct)
} }
/// Amount of obligation tokens for given collateral /// Find collateral by deposit reserve
pub fn collateral_to_obligation_token_amount( pub fn find_collateral_in_deposits(
&self, &self,
collateral_amount: u64, deposit_reserve: Pubkey,
obligation_token_supply: u64, ) -> Result<(&ObligationCollateral, usize), ProgramError> {
) -> Result<u64, ProgramError> { if self.deposits.is_empty() {
let withdraw_pct = msg!("Obligation has no deposits");
Decimal::from(collateral_amount).try_div(self.deposited_collateral_tokens)?; return Err(LendingError::ObligationDepositsEmpty.into());
let token_amount: Decimal = withdraw_pct.try_mul(obligation_token_supply)?;
token_amount.try_floor_u64()
}
/// Accrue interest
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> ProgramResult {
if cumulative_borrow_rate < self.cumulative_borrow_rate_wads {
return Err(LendingError::NegativeInterestRate.into());
} }
let collateral_index = self
let compounded_interest_rate: Rate = cumulative_borrow_rate ._find_collateral_index_in_deposits(deposit_reserve)
.try_div(self.cumulative_borrow_rate_wads)? .ok_or(LendingError::InvalidObligationCollateral)?;
.try_into()?; Ok((&self.deposits[collateral_index], collateral_index))
self.borrowed_liquidity_wads = self
.borrowed_liquidity_wads
.try_mul(compounded_interest_rate)?;
self.cumulative_borrow_rate_wads = cumulative_borrow_rate;
Ok(())
} }
/// Liquidate part of obligation /// Find or add collateral by deposit reserve
pub fn liquidate(&mut self, repay_amount: Decimal, withdraw_amount: u64) -> ProgramResult { pub fn find_or_add_collateral_to_deposits(
self.borrowed_liquidity_wads = self.borrowed_liquidity_wads.try_sub(repay_amount)?;
self.deposited_collateral_tokens = self
.deposited_collateral_tokens
.checked_sub(withdraw_amount)
.ok_or(LendingError::MathOverflow)?;
Ok(())
}
/// Repay borrowed tokens
pub fn repay(
&mut self, &mut self,
liquidity_amount: u64, deposit_reserve: Pubkey,
obligation_token_supply: u64, ) -> Result<&mut ObligationCollateral, ProgramError> {
) -> Result<RepayResult, ProgramError> { if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) {
let decimal_repay_amount = return Ok(&mut self.deposits[collateral_index]);
Decimal::from(liquidity_amount).min(self.borrowed_liquidity_wads);
let integer_repay_amount = decimal_repay_amount.try_ceil_u64()?;
if integer_repay_amount == 0 {
return Err(LendingError::ObligationEmpty.into());
} }
if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
msg!(
"Obligation cannot have more than {} deposits and borrows combined",
MAX_OBLIGATION_RESERVES
);
return Err(LendingError::ObligationReserveLimit.into());
}
let collateral = ObligationCollateral::new(deposit_reserve);
self.deposits.push(collateral);
Ok(self.deposits.last_mut().unwrap())
}
let repay_pct: Decimal = decimal_repay_amount.try_div(self.borrowed_liquidity_wads)?; fn _find_collateral_index_in_deposits(&self, deposit_reserve: Pubkey) -> Option<usize> {
let collateral_withdraw_amount = { self.deposits
let withdraw_amount: Decimal = repay_pct.try_mul(self.deposited_collateral_tokens)?; .iter()
withdraw_amount.try_floor_u64()? .position(|collateral| collateral.deposit_reserve == deposit_reserve)
}; }
let obligation_token_amount = self.collateral_to_obligation_token_amount( /// Find liquidity by borrow reserve
collateral_withdraw_amount, pub fn find_liquidity_in_borrows(
obligation_token_supply, &self,
)?; borrow_reserve: Pubkey,
) -> Result<(&ObligationLiquidity, usize), ProgramError> {
if self.borrows.is_empty() {
msg!("Obligation has no borrows");
return Err(LendingError::ObligationBorrowsEmpty.into());
}
let liquidity_index = self
._find_liquidity_index_in_borrows(borrow_reserve)
.ok_or(LendingError::InvalidObligationLiquidity)?;
Ok((&self.borrows[liquidity_index], liquidity_index))
}
self.liquidate(decimal_repay_amount, collateral_withdraw_amount)?; /// Find or add liquidity by borrow reserve
pub fn find_or_add_liquidity_to_borrows(
&mut self,
borrow_reserve: Pubkey,
) -> Result<&mut ObligationLiquidity, ProgramError> {
if let Some(liquidity_index) = self._find_liquidity_index_in_borrows(borrow_reserve) {
return Ok(&mut self.borrows[liquidity_index]);
}
if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
msg!(
"Obligation cannot have more than {} deposits and borrows combined",
MAX_OBLIGATION_RESERVES
);
return Err(LendingError::ObligationReserveLimit.into());
}
let liquidity = ObligationLiquidity::new(borrow_reserve);
self.borrows.push(liquidity);
Ok(self.borrows.last_mut().unwrap())
}
Ok(RepayResult { fn _find_liquidity_index_in_borrows(&self, borrow_reserve: Pubkey) -> Option<usize> {
collateral_withdraw_amount, self.borrows
obligation_token_amount, .iter()
decimal_repay_amount, .position(|liquidity| liquidity.borrow_reserve == borrow_reserve)
integer_repay_amount,
})
} }
} }
/// Obligation repay result /// Initialize an obligation
pub struct RepayResult { pub struct InitObligationParams {
/// Amount of collateral to withdraw /// Last update to collateral, liquidity, or their market values
pub collateral_withdraw_amount: u64, pub current_slot: Slot,
/// Amount of obligation tokens to burn /// Lending market address
pub obligation_token_amount: u64, pub lending_market: Pubkey,
/// Amount that will be repaid as precise decimal /// Owner authority which can borrow liquidity
pub decimal_repay_amount: Decimal, pub owner: Pubkey,
/// Amount that will be repaid as u64 /// Deposited collateral for the obligation, unique by deposit reserve address
pub integer_repay_amount: u64, pub deposits: Vec<ObligationCollateral>,
} /// Borrowed liquidity for the obligation, unique by borrow reserve address
pub borrows: Vec<ObligationLiquidity>,
/// Create new obligation
pub struct NewObligationParams {
/// Collateral reserve address
pub collateral_reserve: Pubkey,
/// Borrow reserve address
pub borrow_reserve: Pubkey,
/// Obligation token mint address
pub token_mint: Pubkey,
/// Borrow rate used for calculating interest.
pub cumulative_borrow_rate_wads: Decimal,
} }
impl Sealed for Obligation {} impl Sealed for Obligation {}
@ -189,55 +224,275 @@ impl IsInitialized for Obligation {
} }
} }
const OBLIGATION_LEN: usize = 265; /// Obligation collateral state
impl Pack for Obligation { #[derive(Clone, Debug, Default, PartialEq)]
const LEN: usize = 265; pub struct ObligationCollateral {
/// Reserve collateral is deposited to
pub deposit_reserve: Pubkey,
/// Amount of collateral deposited
pub deposited_amount: u64,
/// Collateral market value in quote currency
pub market_value: Decimal,
}
/// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html). impl ObligationCollateral {
fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> { /// Create new obligation collateral
let input = array_ref![input, 0, OBLIGATION_LEN]; pub fn new(deposit_reserve: Pubkey) -> Self {
Self {
deposit_reserve,
deposited_amount: 0,
market_value: Decimal::zero(),
}
}
/// Increase deposited collateral
pub fn deposit(&mut self, collateral_amount: u64) -> ProgramResult {
self.deposited_amount = self
.deposited_amount
.checked_add(collateral_amount)
.ok_or(LendingError::MathOverflow)?;
Ok(())
}
/// Decrease deposited collateral
pub fn withdraw(&mut self, collateral_amount: u64) -> ProgramResult {
self.deposited_amount = self
.deposited_amount
.checked_sub(collateral_amount)
.ok_or(LendingError::MathOverflow)?;
Ok(())
}
}
/// Obligation liquidity state
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ObligationLiquidity {
/// Reserve liquidity is borrowed from
pub borrow_reserve: Pubkey,
/// Borrow rate used for calculating interest
pub cumulative_borrow_rate_wads: Decimal,
/// Amount of liquidity borrowed plus interest
pub borrowed_amount_wads: Decimal,
/// Liquidity market value in quote currency
pub market_value: Decimal,
}
impl ObligationLiquidity {
/// Create new obligation liquidity
pub fn new(borrow_reserve: Pubkey) -> Self {
Self {
borrow_reserve,
cumulative_borrow_rate_wads: Decimal::one(),
borrowed_amount_wads: Decimal::zero(),
market_value: Decimal::zero(),
}
}
/// Decrease borrowed liquidity
pub fn repay(&mut self, settle_amount: Decimal) -> ProgramResult {
self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?;
Ok(())
}
/// Increase borrowed liquidity
pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult {
self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?;
Ok(())
}
/// Accrue interest
pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult {
match cumulative_borrow_rate_wads.cmp(&self.cumulative_borrow_rate_wads) {
Ordering::Less => {
msg!("Interest rate cannot be negative");
return Err(LendingError::NegativeInterestRate.into());
}
Ordering::Equal => {}
Ordering::Greater => {
let compounded_interest_rate: Rate = cumulative_borrow_rate_wads
.try_div(self.cumulative_borrow_rate_wads)?
.try_into()?;
self.borrowed_amount_wads = self
.borrowed_amount_wads
.try_mul(compounded_interest_rate)?;
self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads;
}
}
Ok(())
}
}
const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16
const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16
const OBLIGATION_LEN: usize = 916; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (56 * 1) + (80 * 9)
// @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca
impl Pack for Obligation {
const LEN: usize = OBLIGATION_LEN;
fn pack_into_slice(&self, dst: &mut [u8]) {
let output = array_mut_ref![dst, 0, OBLIGATION_LEN];
#[allow(clippy::ptr_offset_with_cast)] #[allow(clippy::ptr_offset_with_cast)]
let ( let (
version, version,
deposited_collateral_tokens, last_update_slot,
collateral_supply, last_update_stale,
cumulative_borrow_rate, lending_market,
borrowed_liquidity_wads, owner,
borrow_reserve, deposited_value,
token_mint, borrowed_value,
_padding, allowed_borrow_value,
) = array_refs![input, 1, 8, 32, 16, 16, 32, 32, 128]; unhealthy_borrow_value,
Ok(Self { deposits_len,
version: u8::from_le_bytes(*version), borrows_len,
deposited_collateral_tokens: u64::from_le_bytes(*deposited_collateral_tokens), data_flat,
collateral_reserve: Pubkey::new_from_array(*collateral_supply), ) = mut_array_refs![
cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate), output,
borrowed_liquidity_wads: unpack_decimal(borrowed_liquidity_wads), 1,
borrow_reserve: Pubkey::new_from_array(*borrow_reserve), 8,
token_mint: Pubkey::new_from_array(*token_mint), 1,
}) PUBKEY_BYTES,
PUBKEY_BYTES,
16,
16,
16,
16,
1,
1,
OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
];
// obligation
*version = self.version.to_le_bytes();
*last_update_slot = self.last_update.slot.to_le_bytes();
pack_bool(self.last_update.stale, last_update_stale);
lending_market.copy_from_slice(self.lending_market.as_ref());
owner.copy_from_slice(self.owner.as_ref());
pack_decimal(self.deposited_value, deposited_value);
pack_decimal(self.borrowed_value, borrowed_value);
pack_decimal(self.allowed_borrow_value, allowed_borrow_value);
pack_decimal(self.unhealthy_borrow_value, unhealthy_borrow_value);
*deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes();
*borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes();
let mut offset = 0;
// deposits
for collateral in &self.deposits {
let deposits_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (deposit_reserve, deposited_amount, market_value) =
mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16];
deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref());
*deposited_amount = collateral.deposited_amount.to_le_bytes();
pack_decimal(collateral.market_value, market_value);
offset += OBLIGATION_COLLATERAL_LEN;
}
// borrows
for liquidity in &self.borrows {
let borrows_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) =
mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16];
borrow_reserve.copy_from_slice(liquidity.borrow_reserve.as_ref());
pack_decimal(
liquidity.cumulative_borrow_rate_wads,
cumulative_borrow_rate_wads,
);
pack_decimal(liquidity.borrowed_amount_wads, borrowed_amount_wads);
pack_decimal(liquidity.market_value, market_value);
offset += OBLIGATION_LIQUIDITY_LEN;
}
} }
fn pack_into_slice(&self, output: &mut [u8]) { /// Unpacks a byte buffer into an [ObligationInfo](struct.ObligationInfo.html).
let output = array_mut_ref![output, 0, OBLIGATION_LEN]; fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let input = array_ref![src, 0, OBLIGATION_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let ( let (
version, version,
deposited_collateral_tokens, last_update_slot,
collateral_supply, last_update_stale,
cumulative_borrow_rate, lending_market,
borrowed_liquidity_wads, owner,
borrow_reserve, deposited_value,
token_mint, borrowed_value,
_padding, allowed_borrow_value,
) = mut_array_refs![output, 1, 8, 32, 16, 16, 32, 32, 128]; unhealthy_borrow_value,
deposits_len,
borrows_len,
data_flat,
) = array_refs![
input,
1,
8,
1,
PUBKEY_BYTES,
PUBKEY_BYTES,
16,
16,
16,
16,
1,
1,
OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
];
*version = self.version.to_le_bytes(); let version = u8::from_le_bytes(*version);
*deposited_collateral_tokens = self.deposited_collateral_tokens.to_le_bytes(); if version > PROGRAM_VERSION {
collateral_supply.copy_from_slice(self.collateral_reserve.as_ref()); msg!("Obligation version does not match lending program version");
pack_decimal(self.cumulative_borrow_rate_wads, cumulative_borrow_rate); return Err(ProgramError::InvalidAccountData);
pack_decimal(self.borrowed_liquidity_wads, borrowed_liquidity_wads); }
borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref());
token_mint.copy_from_slice(self.token_mint.as_ref()); let deposits_len = u8::from_le_bytes(*deposits_len);
let borrows_len = u8::from_le_bytes(*borrows_len);
let mut deposits = Vec::with_capacity(deposits_len as usize + 1);
let mut borrows = Vec::with_capacity(borrows_len as usize + 1);
let mut offset = 0;
for _ in 0..deposits_len {
let deposits_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (deposit_reserve, deposited_amount, market_value) =
array_refs![deposits_flat, PUBKEY_BYTES, 8, 16];
deposits.push(ObligationCollateral {
deposit_reserve: Pubkey::new(deposit_reserve),
deposited_amount: u64::from_le_bytes(*deposited_amount),
market_value: unpack_decimal(market_value),
});
offset += OBLIGATION_COLLATERAL_LEN;
}
for _ in 0..borrows_len {
let borrows_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) =
array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16];
borrows.push(ObligationLiquidity {
borrow_reserve: Pubkey::new(borrow_reserve),
cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads),
borrowed_amount_wads: unpack_decimal(borrowed_amount_wads),
market_value: unpack_decimal(market_value),
});
offset += OBLIGATION_LIQUIDITY_LEN;
}
Ok(Self {
version,
last_update: LastUpdate {
slot: u64::from_le_bytes(*last_update_slot),
stale: unpack_bool(last_update_stale)?,
},
lending_market: Pubkey::new_from_array(*lending_market),
owner: Pubkey::new_from_array(*owner),
deposits,
borrows,
deposited_value: unpack_decimal(deposited_value),
borrowed_value: unpack_decimal(borrowed_value),
allowed_borrow_value: unpack_decimal(allowed_borrow_value),
unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value),
})
} }
} }
@ -252,28 +507,28 @@ mod test {
#[test] #[test]
fn obligation_accrue_interest_failure() { fn obligation_accrue_interest_failure() {
assert_eq!( assert_eq!(
Obligation { ObligationLiquidity {
cumulative_borrow_rate_wads: Decimal::zero(), cumulative_borrow_rate_wads: Decimal::zero(),
..Obligation::default() ..ObligationLiquidity::default()
} }
.accrue_interest(Decimal::one()), .accrue_interest(Decimal::one()),
Err(LendingError::MathOverflow.into()) Err(LendingError::MathOverflow.into())
); );
assert_eq!( assert_eq!(
Obligation { ObligationLiquidity {
cumulative_borrow_rate_wads: Decimal::from(2u64), cumulative_borrow_rate_wads: Decimal::from(2u64),
..Obligation::default() ..ObligationLiquidity::default()
} }
.accrue_interest(Decimal::one()), .accrue_interest(Decimal::one()),
Err(LendingError::NegativeInterestRate.into()) Err(LendingError::NegativeInterestRate.into())
); );
assert_eq!( assert_eq!(
Obligation { ObligationLiquidity {
cumulative_borrow_rate_wads: Decimal::one(), cumulative_borrow_rate_wads: Decimal::one(),
borrowed_liquidity_wads: Decimal::from(u64::MAX), borrowed_amount_wads: Decimal::from(u64::MAX),
..Obligation::default() ..ObligationLiquidity::default()
} }
.accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)), .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)),
Err(LendingError::MathOverflow.into()) Err(LendingError::MathOverflow.into())
@ -284,7 +539,7 @@ mod test {
prop_compose! { prop_compose! {
fn cumulative_rates()(rate in 1..=u128::MAX)( fn cumulative_rates()(rate in 1..=u128::MAX)(
current_rate in Just(rate), current_rate in Just(rate),
max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128) max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128),
) -> (u128, u128) { ) -> (u128, u128) {
(current_rate, max_new_rate) (current_rate, max_new_rate)
} }
@ -294,89 +549,81 @@ mod test {
// Creates liquidity amounts (repay, borrow) where repay < borrow // Creates liquidity amounts (repay, borrow) where repay < borrow
prop_compose! { prop_compose! {
fn repay_partial_amounts()(repay in 1..=u64::MAX)( fn repay_partial_amounts()(amount in 1..=u64::MAX)(
liquidity_amount in Just(repay), repay_amount in Just(WAD as u128 * amount as u128),
borrowed_liquidity in (WAD as u128 * repay as u128 + 1)..=MAX_BORROWED borrowed_amount in (WAD as u128 * amount as u128 + 1)..=MAX_BORROWED,
) -> (u64, u128) { ) -> (u128, u128) {
(liquidity_amount, borrowed_liquidity) (repay_amount, borrowed_amount)
} }
} }
// Creates liquidity amounts (repay, borrow) where repay >= borrow // Creates liquidity amounts (repay, borrow) where repay >= borrow
prop_compose! { prop_compose! {
fn repay_full_amounts()(repay in 1..=u64::MAX)( fn repay_full_amounts()(amount in 1..=u64::MAX)(
liquidity_amount in Just(repay), repay_amount in Just(WAD as u128 * amount as u128),
borrowed_liquidity in 0..=(WAD as u128 * repay as u128) ) -> (u128, u128) {
) -> (u64, u128) { (repay_amount, repay_amount)
(liquidity_amount, borrowed_liquidity)
}
}
// Creates collateral amounts (collateral, obligation tokens) where c <= ot
prop_compose! {
fn collateral_amounts()(collateral in 1..=u64::MAX)(
deposited_collateral_tokens in Just(collateral),
obligation_tokens in collateral..=u64::MAX
) -> (u64, u64) {
(deposited_collateral_tokens, obligation_tokens)
} }
} }
proptest! { proptest! {
#[test] #[test]
fn repay_partial( fn repay_partial(
(liquidity_amount, borrowed_liquidity) in repay_partial_amounts(), (repay_amount, borrowed_amount) in repay_partial_amounts(),
(deposited_collateral_tokens, obligation_tokens) in collateral_amounts(),
) { ) {
let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity); let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
let mut state = Obligation { deposited_collateral_tokens, borrowed_liquidity_wads, ..Obligation::default() }; let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
let mut obligation = Obligation {
borrows: vec![ObligationLiquidity {
borrowed_amount_wads,
..ObligationLiquidity::default()
}],
..Obligation::default()
};
let repay_result = state.repay(liquidity_amount, obligation_tokens)?; obligation.repay(repay_amount_wads, 0)?;
assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); assert!(obligation.borrows[0].borrowed_amount_wads < borrowed_amount_wads);
assert!(repay_result.collateral_withdraw_amount < deposited_collateral_tokens); assert!(obligation.borrows[0].borrowed_amount_wads > Decimal::zero());
assert!(repay_result.obligation_token_amount < obligation_tokens);
assert!(state.borrowed_liquidity_wads < borrowed_liquidity_wads);
assert!(state.borrowed_liquidity_wads > Decimal::zero());
assert!(state.deposited_collateral_tokens > 0);
let obligation_token_rate = Decimal::from(repay_result.obligation_token_amount).try_div(Decimal::from(obligation_tokens))?;
let collateral_withdraw_rate = Decimal::from(repay_result.collateral_withdraw_amount).try_div(Decimal::from(deposited_collateral_tokens))?;
assert!(obligation_token_rate <= collateral_withdraw_rate);
} }
#[test] #[test]
fn repay_full( fn repay_full(
(liquidity_amount, borrowed_liquidity) in repay_full_amounts(), (repay_amount, borrowed_amount) in repay_full_amounts(),
(deposited_collateral_tokens, obligation_tokens) in collateral_amounts(),
) { ) {
let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity); let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
let mut state = Obligation { deposited_collateral_tokens, borrowed_liquidity_wads, ..Obligation::default() } ; let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
let mut obligation = Obligation {
borrows: vec![ObligationLiquidity {
borrowed_amount_wads,
..ObligationLiquidity::default()
}],
..Obligation::default()
};
let repay_result = state.repay(liquidity_amount, obligation_tokens)?; obligation.repay(repay_amount_wads, 0)?;
assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount)); assert_eq!(obligation.borrows.len(), 0);
assert_eq!(repay_result.collateral_withdraw_amount, deposited_collateral_tokens);
assert_eq!(repay_result.obligation_token_amount, obligation_tokens);
assert_eq!(repay_result.decimal_repay_amount, borrowed_liquidity_wads);
assert_eq!(state.borrowed_liquidity_wads, Decimal::zero());
assert_eq!(state.deposited_collateral_tokens, 0);
} }
#[test] #[test]
fn accrue_interest( fn accrue_interest(
borrowed_liquidity in 0..=u64::MAX,
(current_borrow_rate, new_borrow_rate) in cumulative_rates(), (current_borrow_rate, new_borrow_rate) in cumulative_rates(),
borrowed_amount in 0..=u64::MAX,
) { ) {
let borrowed_liquidity_wads = Decimal::from(borrowed_liquidity);
let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?; let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?;
let mut state = Obligation { cumulative_borrow_rate_wads, borrowed_liquidity_wads, ..Obligation::default() }; let borrowed_amount_wads = Decimal::from(borrowed_amount);
let mut liquidity = ObligationLiquidity {
cumulative_borrow_rate_wads,
borrowed_amount_wads,
..ObligationLiquidity::default()
};
let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?; let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?;
state.accrue_interest(next_cumulative_borrow_rate)?; liquidity.accrue_interest(next_cumulative_borrow_rate)?;
if next_cumulative_borrow_rate > cumulative_borrow_rate_wads { if next_cumulative_borrow_rate > cumulative_borrow_rate_wads {
assert!(state.borrowed_liquidity_wads > borrowed_liquidity_wads); assert!(liquidity.borrowed_amount_wads > borrowed_amount_wads);
} else { } else {
assert!(state.borrowed_liquidity_wads == borrowed_liquidity_wads); assert!(liquidity.borrowed_amount_wads == borrowed_amount_wads);
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More