Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 13de668

Browse files
authored
token-swap: Improve pool token supply on initialization, deposit, and withdrawal (#508)
* token-swap: Add token supply in invariant calculation * Refactor state classes into curve components for future use * Align pool initialization with Uniswap using geometric mean of token amounts * Fix deposit and withdraw instruction to work as a proportion of pool tokens * Add math utilities to calculate the geometric mean with u64 * Improve variable names * Use a fixed starting pool size * Run cargo fmt * Update js tests with new pool numbers * Run linting * Remove math * Fix BN type issues found by flow
1 parent 51c4dc6 commit 13de668

File tree

7 files changed

+296
-165
lines changed

7 files changed

+296
-165
lines changed

bpf-sdk-install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bash
22
set -e
33

4-
channel=${1:-v1.3.9}
4+
channel=${1:-v1.3.12}
55
installDir="$(dirname "$0")"/bin
66
cacheDir=~/.cache/solana-bpf-sdk/"$channel"
77

token-swap/js/cli/token-swap-test.js

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ let tokenAccountB: PublicKey;
3939
const BASE_AMOUNT = 1000;
4040
// Amount passed to instructions
4141
const USER_AMOUNT = 100;
42+
// Pool token amount minted on init
43+
const DEFAULT_POOL_TOKEN_AMOUNT = 1000000000;
44+
// Pool token amount to withdraw / deposit
45+
const POOL_TOKEN_AMOUNT = 1000000;
4246

4347
function assert(condition, message) {
4448
if (!condition) {
@@ -219,16 +223,23 @@ export async function createTokenSwap(): Promise<void> {
219223
}
220224

221225
export async function deposit(): Promise<void> {
226+
const poolMintInfo = await tokenPool.getMintInfo();
227+
const supply = poolMintInfo.supply.toNumber();
228+
const swapTokenA = await mintA.getAccountInfo(tokenAccountA);
229+
const tokenA = (swapTokenA.amount.toNumber() * POOL_TOKEN_AMOUNT) / supply;
230+
const swapTokenB = await mintB.getAccountInfo(tokenAccountB);
231+
const tokenB = (swapTokenB.amount.toNumber() * POOL_TOKEN_AMOUNT) / supply;
232+
222233
console.log('Creating depositor token a account');
223-
let userAccountA = await mintA.createAccount(owner.publicKey);
224-
await mintA.mintTo(userAccountA, owner, [], USER_AMOUNT);
225-
await mintA.approve(userAccountA, authority, owner, [], USER_AMOUNT);
234+
const userAccountA = await mintA.createAccount(owner.publicKey);
235+
await mintA.mintTo(userAccountA, owner, [], tokenA);
236+
await mintA.approve(userAccountA, authority, owner, [], tokenA);
226237
console.log('Creating depositor token b account');
227-
let userAccountB = await mintB.createAccount(owner.publicKey);
228-
await mintB.mintTo(userAccountB, owner, [], USER_AMOUNT);
229-
await mintB.approve(userAccountB, authority, owner, [], USER_AMOUNT);
238+
const userAccountB = await mintB.createAccount(owner.publicKey);
239+
await mintB.mintTo(userAccountB, owner, [], tokenB);
240+
await mintB.approve(userAccountB, authority, owner, [], tokenB);
230241
console.log('Creating depositor pool token account');
231-
let newAccountPool = await tokenPool.createAccount(owner.publicKey);
242+
const newAccountPool = await tokenPool.createAccount(owner.publicKey);
232243
const [tokenProgramId] = await GetPrograms(connection);
233244

234245
console.log('Depositing into swap');
@@ -241,7 +252,7 @@ export async function deposit(): Promise<void> {
241252
tokenPool.publicKey,
242253
newAccountPool,
243254
tokenProgramId,
244-
USER_AMOUNT,
255+
POOL_TOKEN_AMOUNT,
245256
);
246257

247258
let info;
@@ -250,21 +261,34 @@ export async function deposit(): Promise<void> {
250261
info = await mintB.getAccountInfo(userAccountB);
251262
assert(info.amount.toNumber() == 0);
252263
info = await mintA.getAccountInfo(tokenAccountA);
253-
assert(info.amount.toNumber() == BASE_AMOUNT + USER_AMOUNT);
264+
assert(info.amount.toNumber() == BASE_AMOUNT + tokenA);
254265
info = await mintB.getAccountInfo(tokenAccountB);
255-
assert(info.amount.toNumber() == BASE_AMOUNT + USER_AMOUNT);
266+
assert(info.amount.toNumber() == BASE_AMOUNT + tokenB);
256267
info = await tokenPool.getAccountInfo(newAccountPool);
257-
assert(info.amount.toNumber() == USER_AMOUNT);
268+
assert(info.amount.toNumber() == POOL_TOKEN_AMOUNT);
258269
}
259270

260271
export async function withdraw(): Promise<void> {
272+
const poolMintInfo = await tokenPool.getMintInfo();
273+
const supply = poolMintInfo.supply.toNumber();
274+
let swapTokenA = await mintA.getAccountInfo(tokenAccountA);
275+
let swapTokenB = await mintB.getAccountInfo(tokenAccountB);
276+
const tokenA = (swapTokenA.amount.toNumber() * POOL_TOKEN_AMOUNT) / supply;
277+
const tokenB = (swapTokenB.amount.toNumber() * POOL_TOKEN_AMOUNT) / supply;
278+
261279
console.log('Creating withdraw token A account');
262280
let userAccountA = await mintA.createAccount(owner.publicKey);
263281
console.log('Creating withdraw token B account');
264282
let userAccountB = await mintB.createAccount(owner.publicKey);
265283

266284
console.log('Approving withdrawal from pool account');
267-
await tokenPool.approve(tokenAccountPool, authority, owner, [], USER_AMOUNT);
285+
await tokenPool.approve(
286+
tokenAccountPool,
287+
authority,
288+
owner,
289+
[],
290+
POOL_TOKEN_AMOUNT,
291+
);
268292
const [tokenProgramId] = await GetPrograms(connection);
269293

270294
console.log('Withdrawing pool tokens for A and B tokens');
@@ -277,19 +301,23 @@ export async function withdraw(): Promise<void> {
277301
userAccountA,
278302
userAccountB,
279303
tokenProgramId,
280-
USER_AMOUNT,
304+
POOL_TOKEN_AMOUNT,
281305
);
282306

307+
//const poolMintInfo = await tokenPool.getMintInfo();
308+
swapTokenA = await mintA.getAccountInfo(tokenAccountA);
309+
swapTokenB = await mintB.getAccountInfo(tokenAccountB);
310+
283311
let info = await tokenPool.getAccountInfo(tokenAccountPool);
284-
assert(info.amount.toNumber() == BASE_AMOUNT - USER_AMOUNT);
285-
info = await mintA.getAccountInfo(tokenAccountA);
286-
assert(info.amount.toNumber() == BASE_AMOUNT);
287-
info = await mintB.getAccountInfo(tokenAccountB);
288-
assert(info.amount.toNumber() == BASE_AMOUNT);
312+
assert(
313+
info.amount.toNumber() == DEFAULT_POOL_TOKEN_AMOUNT - POOL_TOKEN_AMOUNT,
314+
);
315+
assert(swapTokenA.amount.toNumber() == BASE_AMOUNT);
316+
assert(swapTokenB.amount.toNumber() == BASE_AMOUNT);
289317
info = await mintA.getAccountInfo(userAccountA);
290-
assert(info.amount.toNumber() == USER_AMOUNT);
318+
assert(info.amount.toNumber() == tokenA);
291319
info = await mintB.getAccountInfo(userAccountB);
292-
assert(info.amount.toNumber() == USER_AMOUNT);
320+
assert(info.amount.toNumber() == tokenB);
293321
}
294322

295323
export async function swap(): Promise<void> {
@@ -322,5 +350,7 @@ export async function swap(): Promise<void> {
322350
info = await mintB.getAccountInfo(userAccountB);
323351
assert(info.amount.toNumber() == 69);
324352
info = await tokenPool.getAccountInfo(tokenAccountPool);
325-
assert(info.amount.toNumber() == BASE_AMOUNT - USER_AMOUNT);
353+
assert(
354+
info.amount.toNumber() == DEFAULT_POOL_TOKEN_AMOUNT - POOL_TOKEN_AMOUNT,
355+
);
326356
}

token-swap/js/client/token-swap.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ export class TokenSwap {
419419
* @param poolToken Pool token
420420
* @param poolAccount Pool account to deposit the generated tokens
421421
* @param tokenProgramId Token program id
422-
* @param amount Amount of token A to transfer, token B amount is set by the exchange rate
422+
* @param amount Amount of pool token to deposit, token A and B amount are set by the exchange rate relative to the total pool token supply
423423
*/
424424
async deposit(
425425
authority: PublicKey,
@@ -510,7 +510,7 @@ export class TokenSwap {
510510
* @param userAccountA Token A user account
511511
* @param userAccountB token B user account
512512
* @param tokenProgramId Token program id
513-
* @param amount Amount of token A to transfer, token B amount is set by the exchange rate
513+
* @param amount Amount of pool token to withdraw, token A and B amount are set by the exchange rate relative to the total pool token supply
514514
*/
515515
async withdraw(
516516
authority: PublicKey,

token-swap/program/src/curve.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//! Swap calculations and curve implementations
2+
3+
/// Initial amount of pool tokens for swap contract, hard-coded to something
4+
/// "sensible" given a maximum of u64.
5+
/// Note that on Ethereum, Uniswap uses the geometric mean of all provided
6+
/// input amounts, and Balancer uses 100 * 10 ^ 18.
7+
pub const INITIAL_SWAP_POOL_AMOUNT: u64 = 1_000_000_000;
8+
9+
/// Encodes all results of swapping from a source token to a destination token
10+
pub struct SwapResult {
11+
/// New amount of source token
12+
pub new_source_amount: u64,
13+
/// New amount of destination token
14+
pub new_destination_amount: u64,
15+
/// Amount of destination token swapped
16+
pub amount_swapped: u64,
17+
}
18+
19+
impl SwapResult {
20+
/// SwapResult for swap from one currency into another, given pool information
21+
/// and fee
22+
pub fn swap_to(
23+
source_amount: u64,
24+
swap_source_amount: u64,
25+
swap_destination_amount: u64,
26+
fee_numerator: u64,
27+
fee_denominator: u64,
28+
) -> Option<SwapResult> {
29+
let invariant = swap_source_amount.checked_mul(swap_destination_amount)?;
30+
let new_source_amount = swap_source_amount.checked_add(source_amount)?;
31+
let new_destination_amount = invariant.checked_div(new_source_amount)?;
32+
let remove = swap_destination_amount.checked_sub(new_destination_amount)?;
33+
let fee = remove
34+
.checked_mul(fee_numerator)?
35+
.checked_div(fee_denominator)?;
36+
let new_destination_amount = new_destination_amount.checked_add(fee)?;
37+
let amount_swapped = remove.checked_sub(fee)?;
38+
Some(SwapResult {
39+
new_source_amount,
40+
new_destination_amount,
41+
amount_swapped,
42+
})
43+
}
44+
}
45+
46+
/// The Uniswap invariant calculator.
47+
pub struct ConstantProduct {
48+
/// Token A
49+
pub token_a: u64,
50+
/// Token B
51+
pub token_b: u64,
52+
/// Fee numerator
53+
pub fee_numerator: u64,
54+
/// Fee denominator
55+
pub fee_denominator: u64,
56+
}
57+
58+
impl ConstantProduct {
59+
/// Swap token a to b
60+
pub fn swap_a_to_b(&mut self, token_a: u64) -> Option<u64> {
61+
let result = SwapResult::swap_to(
62+
token_a,
63+
self.token_a,
64+
self.token_b,
65+
self.fee_numerator,
66+
self.fee_denominator,
67+
)?;
68+
self.token_a = result.new_source_amount;
69+
self.token_b = result.new_destination_amount;
70+
Some(result.amount_swapped)
71+
}
72+
73+
/// Swap token b to a
74+
pub fn swap_b_to_a(&mut self, token_b: u64) -> Option<u64> {
75+
let result = SwapResult::swap_to(
76+
token_b,
77+
self.token_b,
78+
self.token_a,
79+
self.fee_numerator,
80+
self.fee_denominator,
81+
)?;
82+
self.token_b = result.new_source_amount;
83+
self.token_a = result.new_destination_amount;
84+
Some(result.amount_swapped)
85+
}
86+
}
87+
88+
/// Conversions for pool tokens, how much to deposit / withdraw, along with
89+
/// proper initialization
90+
pub struct PoolTokenConverter {
91+
/// Total supply
92+
pub supply: u64,
93+
/// Token A amount
94+
pub token_a: u64,
95+
/// Token B amount
96+
pub token_b: u64,
97+
}
98+
99+
impl PoolTokenConverter {
100+
/// Create a converter based on existing market information
101+
pub fn new_existing(supply: u64, token_a: u64, token_b: u64) -> Self {
102+
Self {
103+
supply,
104+
token_a,
105+
token_b,
106+
}
107+
}
108+
109+
/// Create a converter for a new pool token, no supply present yet.
110+
/// According to Uniswap, the geometric mean protects the pool creator
111+
/// in case the initial ratio is off the market.
112+
pub fn new_pool(token_a: u64, token_b: u64) -> Self {
113+
let supply = INITIAL_SWAP_POOL_AMOUNT;
114+
Self {
115+
supply,
116+
token_a,
117+
token_b,
118+
}
119+
}
120+
121+
/// A tokens for pool tokens
122+
pub fn token_a_rate(&self, pool_tokens: u64) -> Option<u64> {
123+
pool_tokens
124+
.checked_mul(self.token_a)?
125+
.checked_div(self.supply)
126+
}
127+
128+
/// B tokens for pool tokens
129+
pub fn token_b_rate(&self, pool_tokens: u64) -> Option<u64> {
130+
pool_tokens
131+
.checked_mul(self.token_b)?
132+
.checked_div(self.supply)
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use super::*;
139+
140+
#[test]
141+
fn initial_pool_amount() {
142+
let token_converter = PoolTokenConverter::new_pool(1, 5);
143+
assert_eq!(token_converter.supply, INITIAL_SWAP_POOL_AMOUNT);
144+
}
145+
146+
fn check_pool_token_a_rate(
147+
token_a: u64,
148+
token_b: u64,
149+
deposit: u64,
150+
supply: u64,
151+
expected: Option<u64>,
152+
) {
153+
let calculator = PoolTokenConverter::new_existing(supply, token_a, token_b);
154+
assert_eq!(calculator.token_a_rate(deposit), expected);
155+
}
156+
157+
#[test]
158+
fn issued_tokens() {
159+
check_pool_token_a_rate(2, 50, 5, 10, Some(1));
160+
check_pool_token_a_rate(10, 10, 5, 10, Some(5));
161+
check_pool_token_a_rate(5, 100, 5, 10, Some(2));
162+
check_pool_token_a_rate(5, u64::MAX, 5, 10, Some(2));
163+
check_pool_token_a_rate(u64::MAX, u64::MAX, 5, 10, None);
164+
}
165+
}

token-swap/program/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
//! An Uniswap-like program for the Solana blockchain.
44
5+
pub mod curve;
56
pub mod entrypoint;
67
pub mod error;
78
pub mod instruction;

0 commit comments

Comments
 (0)