@leofcoin/contracts 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -4
- package/rollup.config.js +48 -0
- package/src/factory.ts +1 -1
- package/src/liquidity-pool-factory.ts +72 -0
- package/src/liquidity-pool.ts +259 -0
- package/src/mock-token.ts +114 -0
- package/src/validators.ts +2 -2
- package/test/factory.js +10 -2
- package/test/liquidity-pool-factory.js +71 -0
- package/test/liquidity-pool.js +175 -0
- package/test/mock-token.js +48 -0
- package/test/native-token.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/contracts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
"./name-service": "./exports/name-service.js",
|
|
9
9
|
"./name-token": "./exports/name-service.js",
|
|
10
10
|
"./power-token": "./exports/power-token.js",
|
|
11
|
+
"./mock-token": "./exports/mock-token.js",
|
|
11
12
|
"./native-token": "./exports/native-token.js",
|
|
13
|
+
"./liquidity-pool": "./exports/liquidity-pool.js",
|
|
14
|
+
"./liquidity-pool-factory": "./exports/liquidity-pool-factory.js",
|
|
12
15
|
"./validators": "./exports/validators.js"
|
|
13
16
|
},
|
|
14
17
|
"scripts": {
|
|
@@ -18,15 +21,16 @@
|
|
|
18
21
|
},
|
|
19
22
|
"repository": {
|
|
20
23
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/leofcoin/monorepo"
|
|
24
|
+
"url": "git+https://github.com/leofcoin/monorepo.git",
|
|
25
|
+
"directory": "packages/contracts"
|
|
22
26
|
},
|
|
23
27
|
"keywords": [],
|
|
24
28
|
"author": "",
|
|
25
29
|
"license": "MIT",
|
|
26
30
|
"bugs": {
|
|
27
|
-
"url": "https://github.com/leofcoin/
|
|
31
|
+
"url": "https://github.com/leofcoin/monorepo/issues"
|
|
28
32
|
},
|
|
29
|
-
"homepage": "https://github.com/leofcoin/contracts#readme",
|
|
33
|
+
"homepage": "https://github.com/leofcoin/monorepo/tree/main/packages/contracts#readme",
|
|
30
34
|
"dependencies": {
|
|
31
35
|
"@leofcoin/standards": "^0.3.7"
|
|
32
36
|
},
|
package/rollup.config.js
CHANGED
|
@@ -82,5 +82,53 @@ export default [
|
|
|
82
82
|
mangle: false
|
|
83
83
|
})
|
|
84
84
|
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
input: './src/liquidity-pool.ts',
|
|
88
|
+
output: {
|
|
89
|
+
dir: './exports',
|
|
90
|
+
format: 'es'
|
|
91
|
+
},
|
|
92
|
+
plugins: [
|
|
93
|
+
resolve({
|
|
94
|
+
mainFields: ['exports']
|
|
95
|
+
}),
|
|
96
|
+
typescript(),
|
|
97
|
+
terser({
|
|
98
|
+
mangle: false
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
input: './src/mock-token.ts',
|
|
104
|
+
output: {
|
|
105
|
+
dir: './exports',
|
|
106
|
+
format: 'es'
|
|
107
|
+
},
|
|
108
|
+
plugins: [
|
|
109
|
+
resolve({
|
|
110
|
+
mainFields: ['exports']
|
|
111
|
+
}),
|
|
112
|
+
typescript(),
|
|
113
|
+
terser({
|
|
114
|
+
mangle: false
|
|
115
|
+
})
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
input: './src/liquidity-pool-factory.ts',
|
|
120
|
+
output: {
|
|
121
|
+
dir: './exports',
|
|
122
|
+
format: 'es'
|
|
123
|
+
},
|
|
124
|
+
plugins: [
|
|
125
|
+
resolve({
|
|
126
|
+
mainFields: ['exports']
|
|
127
|
+
}),
|
|
128
|
+
typescript(),
|
|
129
|
+
terser({
|
|
130
|
+
mangle: false
|
|
131
|
+
})
|
|
132
|
+
]
|
|
85
133
|
}
|
|
86
134
|
]
|
package/src/factory.ts
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import Meta, { MetaState } from '@leofcoin/standards/meta.js'
|
|
2
|
+
|
|
3
|
+
export interface LiquidityPoolFactoryState extends MetaState {
|
|
4
|
+
pools: address[]
|
|
5
|
+
poolsByToken: { [token0: address]: { [token1: address]: address } }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const canonicalPair = (tokenA: address, tokenB: address): [address, address] => {
|
|
9
|
+
if (!tokenA || !tokenB) throw new Error('token address undefined')
|
|
10
|
+
if (tokenA === tokenB) throw new Error('pool tokens must differ')
|
|
11
|
+
return [tokenA, tokenB].sort((left, right) => left.localeCompare(right)) as [address, address]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default class LiquidityPoolFactory extends Meta {
|
|
15
|
+
#pools: address[] = []
|
|
16
|
+
#poolsByToken: { [token0: address]: { [token1: address]: address } } = {}
|
|
17
|
+
|
|
18
|
+
constructor(state?: LiquidityPoolFactoryState) {
|
|
19
|
+
super(state)
|
|
20
|
+
if (state) {
|
|
21
|
+
this.#pools = [...state.pools]
|
|
22
|
+
this.#poolsByToken = Object.fromEntries(
|
|
23
|
+
Object.entries(state.poolsByToken).map(([token0, pools]) => [token0, { ...pools }])
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get state(): LiquidityPoolFactoryState {
|
|
29
|
+
return {
|
|
30
|
+
...(super.state as MetaState),
|
|
31
|
+
pools: [...this.#pools],
|
|
32
|
+
poolsByToken: Object.fromEntries(
|
|
33
|
+
Object.entries(this.#poolsByToken).map(([token0, pools]) => [token0, { ...pools }])
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get pools() {
|
|
39
|
+
return [...this.#pools]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get totalPools() {
|
|
43
|
+
return BigInt(this.#pools.length)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
poolFor(tokenA: address, tokenB: address): address | undefined {
|
|
47
|
+
const [token0, token1] = canonicalPair(tokenA, tokenB)
|
|
48
|
+
return this.#poolsByToken[token0]?.[token1]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async registerPool(pool: address): Promise<address> {
|
|
52
|
+
if (!pool) throw new Error('pool address undefined')
|
|
53
|
+
if (this.#pools.includes(pool)) throw new Error('pool already registered')
|
|
54
|
+
if (msg.sender !== this.creator) throw new Error('only the factory owner may register pools')
|
|
55
|
+
|
|
56
|
+
const creator = (await msg.staticCall(pool, 'creator')) as address
|
|
57
|
+
if (creator !== msg.sender) throw new Error('factory owner must deploy the pool')
|
|
58
|
+
|
|
59
|
+
const poolToken0 = (await msg.staticCall(pool, 'token0')) as address
|
|
60
|
+
const poolToken1 = (await msg.staticCall(pool, 'token1')) as address
|
|
61
|
+
const [token0, token1] = canonicalPair(poolToken0, poolToken1)
|
|
62
|
+
|
|
63
|
+
const fee = BigInt(await msg.staticCall(pool, 'feeBasisPoints'))
|
|
64
|
+
if (fee < 0n || fee >= 10_000n) throw new Error('invalid pool fee')
|
|
65
|
+
if (this.#poolsByToken[token0]?.[token1]) throw new Error('pair already registered')
|
|
66
|
+
|
|
67
|
+
if (!this.#poolsByToken[token0]) this.#poolsByToken[token0] = {}
|
|
68
|
+
this.#poolsByToken[token0][token1] = pool
|
|
69
|
+
this.#pools.push(pool)
|
|
70
|
+
return pool
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import Meta, { MetaState } from '@leofcoin/standards/meta.js'
|
|
2
|
+
|
|
3
|
+
export interface LiquidityPoolState extends MetaState {
|
|
4
|
+
token0: address
|
|
5
|
+
token1: address
|
|
6
|
+
feeBasisPoints: bigint
|
|
7
|
+
reserve0: bigint
|
|
8
|
+
reserve1: bigint
|
|
9
|
+
totalLiquidity: bigint
|
|
10
|
+
liquidity: { [provider: address]: bigint }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const BASIS_POINTS = 10_000n
|
|
14
|
+
|
|
15
|
+
const squareRoot = (value: bigint): bigint => {
|
|
16
|
+
if (value < 0n) throw new Error('cannot calculate square root of a negative value')
|
|
17
|
+
if (value < 2n) return value
|
|
18
|
+
|
|
19
|
+
let current = value
|
|
20
|
+
let next = (current + value / current) / 2n
|
|
21
|
+
while (next < current) {
|
|
22
|
+
current = next
|
|
23
|
+
next = (current + value / current) / 2n
|
|
24
|
+
}
|
|
25
|
+
return current
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default class LiquidityPool extends Meta {
|
|
29
|
+
#token0: address
|
|
30
|
+
#token1: address
|
|
31
|
+
#feeBasisPoints: bigint
|
|
32
|
+
#reserve0 = 0n
|
|
33
|
+
#reserve1 = 0n
|
|
34
|
+
#totalLiquidity = 0n
|
|
35
|
+
#liquidity: { [provider: address]: bigint } = {}
|
|
36
|
+
#locked = false
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
token0: address,
|
|
40
|
+
token1: address,
|
|
41
|
+
feeBasisPoints: bigint = 30n,
|
|
42
|
+
state?: LiquidityPoolState
|
|
43
|
+
) {
|
|
44
|
+
if (!token0 || !token1) throw new Error('token address undefined')
|
|
45
|
+
if (token0 === token1) throw new Error('pool tokens must differ')
|
|
46
|
+
super(state)
|
|
47
|
+
this.#token0 = token0
|
|
48
|
+
this.#token1 = token1
|
|
49
|
+
this.#feeBasisPoints = BigInt(state?.feeBasisPoints ?? feeBasisPoints)
|
|
50
|
+
if (this.#feeBasisPoints < 0n || this.#feeBasisPoints >= BASIS_POINTS)
|
|
51
|
+
throw new Error('invalid fee')
|
|
52
|
+
|
|
53
|
+
if (state) {
|
|
54
|
+
if (state.token0 !== token0 || state.token1 !== token1)
|
|
55
|
+
throw new Error('pool state token mismatch')
|
|
56
|
+
this.#reserve0 = BigInt(state.reserve0)
|
|
57
|
+
this.#reserve1 = BigInt(state.reserve1)
|
|
58
|
+
this.#totalLiquidity = BigInt(state.totalLiquidity)
|
|
59
|
+
this.#liquidity = Object.fromEntries(
|
|
60
|
+
Object.entries(state.liquidity).map(([provider, amount]) => [provider, BigInt(amount)])
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get token0() {
|
|
66
|
+
return this.#token0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get token1() {
|
|
70
|
+
return this.#token1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get feeBasisPoints() {
|
|
74
|
+
return this.#feeBasisPoints
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get reserve0() {
|
|
78
|
+
return this.#reserve0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get reserve1() {
|
|
82
|
+
return this.#reserve1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get reserves() {
|
|
86
|
+
return [this.#reserve0, this.#reserve1]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get totalLiquidity() {
|
|
90
|
+
return this.#totalLiquidity
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get state(): LiquidityPoolState {
|
|
94
|
+
return {
|
|
95
|
+
...(super.state as MetaState),
|
|
96
|
+
token0: this.#token0,
|
|
97
|
+
token1: this.#token1,
|
|
98
|
+
feeBasisPoints: this.#feeBasisPoints,
|
|
99
|
+
reserve0: this.#reserve0,
|
|
100
|
+
reserve1: this.#reserve1,
|
|
101
|
+
totalLiquidity: this.#totalLiquidity,
|
|
102
|
+
liquidity: { ...this.#liquidity }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
liquidityOf(provider: address): bigint {
|
|
107
|
+
return this.#liquidity[provider] ?? 0n
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getAmountOut(tokenIn: address, amountIn: bigint): bigint {
|
|
111
|
+
amountIn = BigInt(amountIn)
|
|
112
|
+
if (amountIn <= 0n) throw new Error('input amount must be positive')
|
|
113
|
+
|
|
114
|
+
let reserveIn: bigint
|
|
115
|
+
let reserveOut: bigint
|
|
116
|
+
if (tokenIn === this.#token0) {
|
|
117
|
+
reserveIn = this.#reserve0
|
|
118
|
+
reserveOut = this.#reserve1
|
|
119
|
+
} else if (tokenIn === this.#token1) {
|
|
120
|
+
reserveIn = this.#reserve1
|
|
121
|
+
reserveOut = this.#reserve0
|
|
122
|
+
} else {
|
|
123
|
+
throw new Error('token not in pool')
|
|
124
|
+
}
|
|
125
|
+
if (reserveIn <= 0n || reserveOut <= 0n) throw new Error('insufficient liquidity')
|
|
126
|
+
|
|
127
|
+
const amountInWithFee = amountIn * (BASIS_POINTS - this.#feeBasisPoints)
|
|
128
|
+
return (amountInWithFee * reserveOut) / (reserveIn * BASIS_POINTS + amountInWithFee)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async addLiquidity(
|
|
132
|
+
amount0Desired: bigint,
|
|
133
|
+
amount1Desired: bigint,
|
|
134
|
+
minimumLiquidity: bigint = 0n
|
|
135
|
+
) {
|
|
136
|
+
return this.#withLock(async () => {
|
|
137
|
+
amount0Desired = BigInt(amount0Desired)
|
|
138
|
+
amount1Desired = BigInt(amount1Desired)
|
|
139
|
+
minimumLiquidity = BigInt(minimumLiquidity)
|
|
140
|
+
if (amount0Desired <= 0n || amount1Desired <= 0n)
|
|
141
|
+
throw new Error('liquidity amounts must be positive')
|
|
142
|
+
if (minimumLiquidity < 0n) throw new Error('minimum liquidity cannot be negative')
|
|
143
|
+
|
|
144
|
+
let amount0 = amount0Desired
|
|
145
|
+
let amount1 = amount1Desired
|
|
146
|
+
let minted: bigint
|
|
147
|
+
if (this.#totalLiquidity === 0n) {
|
|
148
|
+
minted = squareRoot(amount0 * amount1)
|
|
149
|
+
} else {
|
|
150
|
+
const amount1ForAmount0 = (amount0Desired * this.#reserve1) / this.#reserve0
|
|
151
|
+
if (amount1ForAmount0 <= amount1Desired) {
|
|
152
|
+
amount1 = amount1ForAmount0
|
|
153
|
+
} else {
|
|
154
|
+
amount0 = (amount1Desired * this.#reserve0) / this.#reserve1
|
|
155
|
+
}
|
|
156
|
+
minted = (amount0 * this.#totalLiquidity) / this.#reserve0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (amount0 <= 0n || amount1 <= 0n || minted <= 0n)
|
|
160
|
+
throw new Error('liquidity amount rounds to zero')
|
|
161
|
+
if (minted < minimumLiquidity) throw new Error('minimum liquidity not met')
|
|
162
|
+
|
|
163
|
+
const provider = msg.sender
|
|
164
|
+
this.#reserve0 += amount0
|
|
165
|
+
this.#reserve1 += amount1
|
|
166
|
+
this.#totalLiquidity += minted
|
|
167
|
+
this.#liquidity[provider] = this.liquidityOf(provider) + minted
|
|
168
|
+
|
|
169
|
+
await this.#pull(this.#token0, provider, amount0)
|
|
170
|
+
await this.#pull(this.#token1, provider, amount1)
|
|
171
|
+
return { amount0, amount1, liquidity: minted }
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async removeLiquidity(
|
|
176
|
+
liquidity: bigint,
|
|
177
|
+
minimumAmount0: bigint = 0n,
|
|
178
|
+
minimumAmount1: bigint = 0n,
|
|
179
|
+
receiver: address = msg.sender
|
|
180
|
+
) {
|
|
181
|
+
return this.#withLock(async () => {
|
|
182
|
+
liquidity = BigInt(liquidity)
|
|
183
|
+
minimumAmount0 = BigInt(minimumAmount0)
|
|
184
|
+
minimumAmount1 = BigInt(minimumAmount1)
|
|
185
|
+
if (!receiver) throw new Error('receiver undefined')
|
|
186
|
+
if (liquidity <= 0n) throw new Error('liquidity must be positive')
|
|
187
|
+
if (this.liquidityOf(msg.sender) < liquidity)
|
|
188
|
+
throw new Error('insufficient liquidity balance')
|
|
189
|
+
|
|
190
|
+
const amount0 = (liquidity * this.#reserve0) / this.#totalLiquidity
|
|
191
|
+
const amount1 = (liquidity * this.#reserve1) / this.#totalLiquidity
|
|
192
|
+
if (amount0 <= 0n || amount1 <= 0n) throw new Error('withdrawal amount rounds to zero')
|
|
193
|
+
if (amount0 < minimumAmount0 || amount1 < minimumAmount1)
|
|
194
|
+
throw new Error('minimum withdrawal amount not met')
|
|
195
|
+
|
|
196
|
+
this.#liquidity[msg.sender] -= liquidity
|
|
197
|
+
this.#totalLiquidity -= liquidity
|
|
198
|
+
this.#reserve0 -= amount0
|
|
199
|
+
this.#reserve1 -= amount1
|
|
200
|
+
|
|
201
|
+
await this.#push(this.#token0, receiver, amount0)
|
|
202
|
+
await this.#push(this.#token1, receiver, amount1)
|
|
203
|
+
return { amount0, amount1, liquidity }
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async swapExactTokensForTokens(
|
|
208
|
+
tokenIn: address,
|
|
209
|
+
amountIn: bigint,
|
|
210
|
+
minimumAmountOut: bigint,
|
|
211
|
+
receiver: address = msg.sender,
|
|
212
|
+
deadline: number = Number.MAX_SAFE_INTEGER
|
|
213
|
+
) {
|
|
214
|
+
return this.#withLock(async () => {
|
|
215
|
+
amountIn = BigInt(amountIn)
|
|
216
|
+
minimumAmountOut = BigInt(minimumAmountOut)
|
|
217
|
+
if (!receiver) throw new Error('receiver undefined')
|
|
218
|
+
if (!Number.isSafeInteger(deadline) || deadline < Date.now()) throw new Error('swap expired')
|
|
219
|
+
|
|
220
|
+
const amountOut = this.getAmountOut(tokenIn, amountIn)
|
|
221
|
+
if (amountOut <= 0n) throw new Error('output amount rounds to zero')
|
|
222
|
+
if (amountOut < minimumAmountOut) throw new Error('minimum output amount not met')
|
|
223
|
+
|
|
224
|
+
const sender = msg.sender
|
|
225
|
+
if (tokenIn === this.#token0) {
|
|
226
|
+
this.#reserve0 += amountIn
|
|
227
|
+
this.#reserve1 -= amountOut
|
|
228
|
+
await this.#pull(this.#token0, sender, amountIn)
|
|
229
|
+
await this.#push(this.#token1, receiver, amountOut)
|
|
230
|
+
} else if (tokenIn === this.#token1) {
|
|
231
|
+
this.#reserve1 += amountIn
|
|
232
|
+
this.#reserve0 -= amountOut
|
|
233
|
+
await this.#pull(this.#token1, sender, amountIn)
|
|
234
|
+
await this.#push(this.#token0, receiver, amountOut)
|
|
235
|
+
} else {
|
|
236
|
+
throw new Error('token not in pool')
|
|
237
|
+
}
|
|
238
|
+
return amountOut
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async #pull(token: address, owner: address, amount: bigint) {
|
|
243
|
+
return msg.call(token, 'transferFrom', [owner, msg.contract, amount])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async #push(token: address, receiver: address, amount: bigint) {
|
|
247
|
+
return msg.call(token, 'transfer', [receiver, amount])
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async #withLock<T>(operation: () => Promise<T>): Promise<T> {
|
|
251
|
+
if (this.#locked) throw new Error('reentrant call')
|
|
252
|
+
this.#locked = true
|
|
253
|
+
try {
|
|
254
|
+
return await operation()
|
|
255
|
+
} finally {
|
|
256
|
+
this.#locked = false
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import Token, { TokenState } from '@leofcoin/standards/token.js'
|
|
2
|
+
|
|
3
|
+
export interface MockTokenAllocation {
|
|
4
|
+
address: address
|
|
5
|
+
amount: bigint | string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface MockTokenState extends TokenState {
|
|
9
|
+
name: string
|
|
10
|
+
symbol: string
|
|
11
|
+
decimals: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const validateMetadata = (name: string, symbol: string, decimals: number) => {
|
|
15
|
+
if (!name?.trim()) throw new Error('token name is required')
|
|
16
|
+
if (!symbol?.trim()) throw new Error('token symbol is required')
|
|
17
|
+
if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 18)
|
|
18
|
+
throw new Error('token decimals must be an integer between 0 and 18')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const initialState = (
|
|
22
|
+
name: string,
|
|
23
|
+
symbol: string,
|
|
24
|
+
decimals: number,
|
|
25
|
+
initialSupply: bigint,
|
|
26
|
+
allocations: MockTokenAllocation[]
|
|
27
|
+
): MockTokenState => {
|
|
28
|
+
initialSupply = BigInt(initialSupply)
|
|
29
|
+
if (initialSupply <= 0n) throw new Error('initial supply must be positive')
|
|
30
|
+
|
|
31
|
+
const effectiveAllocations = allocations.length > 0 ? allocations : [{ address: msg.sender, amount: initialSupply }]
|
|
32
|
+
const balances: Record<address, bigint> = {}
|
|
33
|
+
let allocated = 0n
|
|
34
|
+
|
|
35
|
+
for (const allocation of [...effectiveAllocations].sort((left, right) => left.address.localeCompare(right.address))) {
|
|
36
|
+
if (!allocation.address || balances[allocation.address] !== undefined)
|
|
37
|
+
throw new Error('invalid or duplicate allocation address')
|
|
38
|
+
const amount = BigInt(allocation.amount)
|
|
39
|
+
if (amount <= 0n) throw new Error('allocation amount must be positive')
|
|
40
|
+
balances[allocation.address] = amount
|
|
41
|
+
allocated += amount
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (allocated !== initialSupply) {
|
|
45
|
+
throw new Error(`allocations must equal initial supply: expected ${initialSupply}, got ${allocated}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
symbol,
|
|
51
|
+
decimals,
|
|
52
|
+
creator: msg.sender,
|
|
53
|
+
createdAt: BigInt(Date.now()),
|
|
54
|
+
roles: {
|
|
55
|
+
OWNER: [msg.sender],
|
|
56
|
+
MINT: [msg.sender],
|
|
57
|
+
BURN: [msg.sender]
|
|
58
|
+
},
|
|
59
|
+
holders: BigInt(Object.keys(balances).length),
|
|
60
|
+
balances,
|
|
61
|
+
approvals: {},
|
|
62
|
+
totalSupply: initialSupply
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Development-only fungible asset used to test generic token and DEX behavior.
|
|
68
|
+
* A deployment of this contract never represents or escrows the external asset
|
|
69
|
+
* whose name or symbol it mimics.
|
|
70
|
+
*/
|
|
71
|
+
export default class MockToken extends Token {
|
|
72
|
+
#tokenName: string
|
|
73
|
+
#tokenSymbol: string
|
|
74
|
+
#tokenDecimals: number
|
|
75
|
+
|
|
76
|
+
constructor(
|
|
77
|
+
name: string,
|
|
78
|
+
symbol: string,
|
|
79
|
+
decimals: number,
|
|
80
|
+
initialSupply: bigint,
|
|
81
|
+
allocations: MockTokenAllocation[] = [],
|
|
82
|
+
restoredState?: MockTokenState
|
|
83
|
+
) {
|
|
84
|
+
validateMetadata(name, symbol, decimals)
|
|
85
|
+
const state = restoredState || initialState(name, symbol, decimals, initialSupply, allocations)
|
|
86
|
+
if (state.name !== name || state.symbol !== symbol || state.decimals !== decimals)
|
|
87
|
+
throw new Error('restored token metadata mismatch')
|
|
88
|
+
super(name, symbol, decimals, state)
|
|
89
|
+
this.#tokenName = name
|
|
90
|
+
this.#tokenSymbol = symbol
|
|
91
|
+
this.#tokenDecimals = decimals
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get name() {
|
|
95
|
+
return this.#tokenName
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get symbol() {
|
|
99
|
+
return this.#tokenSymbol
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get decimals() {
|
|
103
|
+
return this.#tokenDecimals
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get state(): MockTokenState {
|
|
107
|
+
return {
|
|
108
|
+
...super.state,
|
|
109
|
+
name: this.#tokenName,
|
|
110
|
+
symbol: this.#tokenSymbol,
|
|
111
|
+
decimals: this.#tokenDecimals
|
|
112
|
+
} as MockTokenState
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/validators.ts
CHANGED
|
@@ -126,7 +126,7 @@ export default class Validators extends Roles {
|
|
|
126
126
|
if (this.minimumBalance > balance)
|
|
127
127
|
throw new Error(`balance to low! got: ${balance} need: ${this.#minimumBalance}`)
|
|
128
128
|
|
|
129
|
-
await msg.call(this.currency, '
|
|
129
|
+
await msg.call(this.currency, 'transferFrom', [validator, msg.contract, this.#minimumBalance])
|
|
130
130
|
|
|
131
131
|
this.#balances[validator] = this.#minimumBalance
|
|
132
132
|
this.#validators.push(validator)
|
|
@@ -135,7 +135,7 @@ export default class Validators extends Roles {
|
|
|
135
135
|
async removeValidator(validator) {
|
|
136
136
|
this.#isAllowed(validator)
|
|
137
137
|
if (!this.has(validator)) throw new Error('validator not found')
|
|
138
|
-
await msg.call(this.currency, 'transfer', [
|
|
138
|
+
await msg.call(this.currency, 'transfer', [validator, this.#minimumBalance])
|
|
139
139
|
delete this.#balances[validator]
|
|
140
140
|
this.#validators.splice(this.#validators.indexOf(validator), 1)
|
|
141
141
|
}
|
package/test/factory.js
CHANGED
|
@@ -40,10 +40,11 @@ describe('Factory', () => {
|
|
|
40
40
|
return BigInt(100) // Balance too low
|
|
41
41
|
} else if (method === 'creator') {
|
|
42
42
|
return true
|
|
43
|
-
} else if (method === 'transfer') {
|
|
44
|
-
throw new Error('amount exceeds balance')
|
|
45
43
|
}
|
|
46
44
|
}
|
|
45
|
+
global.msg.call = async (currency, method) => {
|
|
46
|
+
if (method === 'transferFrom') throw new Error('amount exceeds balance')
|
|
47
|
+
}
|
|
47
48
|
await assert.rejects(async () => await factory.registerContract('0xContractAddress'), {
|
|
48
49
|
message: 'amount exceeds balance'
|
|
49
50
|
})
|
|
@@ -57,9 +58,16 @@ describe('Factory', () => {
|
|
|
57
58
|
})
|
|
58
59
|
|
|
59
60
|
it('should successfully register a contract', async () => {
|
|
61
|
+
let creatorParameters
|
|
62
|
+
global.msg.staticCall = async (currency, method, args) => {
|
|
63
|
+
if (method === 'creator') creatorParameters = args
|
|
64
|
+
if (method === 'balanceOf') return BigInt(100000)
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
60
67
|
await factory.registerContract('0xContractAddress')
|
|
61
68
|
|
|
62
69
|
assert.equal(factory.totalContracts, BigInt(1))
|
|
63
70
|
assert.ok(factory.contracts.includes('0xContractAddress'))
|
|
71
|
+
assert.equal(creatorParameters, undefined)
|
|
64
72
|
})
|
|
65
73
|
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { beforeEach, describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import LiquidityPoolFactory from '../exports/liquidity-pool-factory.js'
|
|
4
|
+
|
|
5
|
+
describe('LiquidityPoolFactory', () => {
|
|
6
|
+
const creator = '0xCreator'
|
|
7
|
+
const token0 = '0xToken0'
|
|
8
|
+
const token1 = '0xToken1'
|
|
9
|
+
const poolAddress = '0xPool'
|
|
10
|
+
let factory
|
|
11
|
+
let pool
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
pool = { creator, token0, token1, feeBasisPoints: 30n }
|
|
15
|
+
global.msg = {
|
|
16
|
+
sender: creator,
|
|
17
|
+
contract: '0xFactory',
|
|
18
|
+
staticCall: async (target, method) => {
|
|
19
|
+
assert.equal(target, poolAddress)
|
|
20
|
+
return pool[method]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
factory = new LiquidityPoolFactory()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('registers one canonical pool per pair', async () => {
|
|
27
|
+
assert.equal(await factory.registerPool(poolAddress), poolAddress)
|
|
28
|
+
assert.equal(factory.poolFor(token0, token1), poolAddress)
|
|
29
|
+
assert.equal(factory.poolFor(token1, token0), poolAddress)
|
|
30
|
+
assert.deepEqual(factory.pools, [poolAddress])
|
|
31
|
+
assert.equal(factory.totalPools, 1n)
|
|
32
|
+
|
|
33
|
+
await assert.rejects(factory.registerPool(poolAddress), {
|
|
34
|
+
message: 'pool already registered'
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('requires the factory owner and accepts either pool token order', async () => {
|
|
39
|
+
global.msg.sender = '0xAttacker'
|
|
40
|
+
await assert.rejects(factory.registerPool(poolAddress), {
|
|
41
|
+
message: 'only the factory owner may register pools'
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
global.msg.sender = creator
|
|
45
|
+
pool = { ...pool, token0: token1, token1: token0 }
|
|
46
|
+
assert.equal(await factory.registerPool(poolAddress), poolAddress)
|
|
47
|
+
assert.equal(factory.poolFor(token0, token1), poolAddress)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('requires pools to be deployed by the factory owner', async () => {
|
|
51
|
+
pool.creator = '0xDifferentCreator'
|
|
52
|
+
await assert.rejects(factory.registerPool(poolAddress), {
|
|
53
|
+
message: 'factory owner must deploy the pool'
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('rejects a second pool for the same pair and restores state', async () => {
|
|
58
|
+
await factory.registerPool(poolAddress)
|
|
59
|
+
const restored = new LiquidityPoolFactory(factory.state)
|
|
60
|
+
assert.equal(restored.poolFor(token1, token0), poolAddress)
|
|
61
|
+
|
|
62
|
+
const secondPool = '0xSecondPool'
|
|
63
|
+
global.msg.staticCall = async (target, method) => {
|
|
64
|
+
assert.equal(target, secondPool)
|
|
65
|
+
return pool[method]
|
|
66
|
+
}
|
|
67
|
+
await assert.rejects(restored.registerPool(secondPool), {
|
|
68
|
+
message: 'pair already registered'
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { beforeEach, describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import LiquidityPool from '../exports/liquidity-pool.js'
|
|
4
|
+
import MockToken from '../exports/mock-token.js'
|
|
5
|
+
|
|
6
|
+
describe('LiquidityPool', () => {
|
|
7
|
+
const poolAddress = '0xPool'
|
|
8
|
+
const token0 = '0xToken0'
|
|
9
|
+
const token1 = '0xToken1'
|
|
10
|
+
const alice = '0xAlice'
|
|
11
|
+
const bob = '0xBob'
|
|
12
|
+
let pool
|
|
13
|
+
let balances
|
|
14
|
+
let allowances
|
|
15
|
+
|
|
16
|
+
const balanceOf = (token, account) => balances[token][account] ?? 0n
|
|
17
|
+
const allowanceOf = (token, owner, operator) => allowances[token][owner]?.[operator] ?? 0n
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
balances = {
|
|
21
|
+
[token0]: { [alice]: 100_000n, [bob]: 100_000n },
|
|
22
|
+
[token1]: { [alice]: 100_000n, [bob]: 100_000n }
|
|
23
|
+
}
|
|
24
|
+
allowances = {
|
|
25
|
+
[token0]: {
|
|
26
|
+
[alice]: { [poolAddress]: 100_000n },
|
|
27
|
+
[bob]: { [poolAddress]: 100_000n }
|
|
28
|
+
},
|
|
29
|
+
[token1]: {
|
|
30
|
+
[alice]: { [poolAddress]: 100_000n },
|
|
31
|
+
[bob]: { [poolAddress]: 100_000n }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
global.msg = {
|
|
35
|
+
sender: alice,
|
|
36
|
+
contract: poolAddress,
|
|
37
|
+
call: async (contract, method, params) => {
|
|
38
|
+
if (method === 'transferFrom') {
|
|
39
|
+
const [owner, receiver, amount] = params
|
|
40
|
+
const approved = allowanceOf(contract, owner, poolAddress)
|
|
41
|
+
if (approved < amount) throw new Error('amount exceeds allowance')
|
|
42
|
+
if (balanceOf(contract, owner) < amount) throw new Error('amount exceeds balance')
|
|
43
|
+
allowances[contract][owner][poolAddress] = approved - amount
|
|
44
|
+
balances[contract][owner] -= amount
|
|
45
|
+
balances[contract][receiver] = balanceOf(contract, receiver) + amount
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (method === 'transfer') {
|
|
49
|
+
const [receiver, amount] = params
|
|
50
|
+
if (balanceOf(contract, poolAddress) < amount) throw new Error('amount exceeds balance')
|
|
51
|
+
balances[contract][poolAddress] -= amount
|
|
52
|
+
balances[contract][receiver] = balanceOf(contract, receiver) + amount
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`unsupported token method: ${method}`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
pool = new LiquidityPool(token0, token1)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('adds balanced initial liquidity using approvals', async () => {
|
|
62
|
+
const result = await pool.addLiquidity(10_000n, 10_000n, 10_000n)
|
|
63
|
+
|
|
64
|
+
assert.deepEqual(result, {
|
|
65
|
+
amount0: 10_000n,
|
|
66
|
+
amount1: 10_000n,
|
|
67
|
+
liquidity: 10_000n
|
|
68
|
+
})
|
|
69
|
+
assert.deepEqual(pool.reserves, [10_000n, 10_000n])
|
|
70
|
+
assert.equal(pool.liquidityOf(alice), 10_000n)
|
|
71
|
+
assert.equal(balanceOf(token0, poolAddress), 10_000n)
|
|
72
|
+
assert.equal(balanceOf(token1, poolAddress), 10_000n)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('accepts only the proportional part of later liquidity', async () => {
|
|
76
|
+
await pool.addLiquidity(10_000n, 20_000n)
|
|
77
|
+
global.msg.sender = bob
|
|
78
|
+
|
|
79
|
+
const result = await pool.addLiquidity(10_000n, 30_000n)
|
|
80
|
+
|
|
81
|
+
assert.deepEqual(result, {
|
|
82
|
+
amount0: 10_000n,
|
|
83
|
+
amount1: 20_000n,
|
|
84
|
+
liquidity: 14_142n
|
|
85
|
+
})
|
|
86
|
+
assert.deepEqual(pool.reserves, [20_000n, 40_000n])
|
|
87
|
+
assert.equal(balanceOf(token1, bob), 80_000n)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('swaps with a constant-product fee and enforces slippage', async () => {
|
|
91
|
+
await pool.addLiquidity(10_000n, 10_000n)
|
|
92
|
+
global.msg.sender = bob
|
|
93
|
+
|
|
94
|
+
const quoted = pool.getAmountOut(token0, 1_000n)
|
|
95
|
+
assert.equal(quoted, 906n)
|
|
96
|
+
const productBefore = pool.reserve0 * pool.reserve1
|
|
97
|
+
await assert.rejects(pool.swapExactTokensForTokens(token0, 1_000n, 907n, bob, Date.now()), {
|
|
98
|
+
message: 'minimum output amount not met'
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const received = await pool.swapExactTokensForTokens(token0, 1_000n, 906n, bob, Date.now())
|
|
102
|
+
assert.equal(received, 906n)
|
|
103
|
+
assert.deepEqual(pool.reserves, [11_000n, 9_094n])
|
|
104
|
+
assert.ok(pool.reserve0 * pool.reserve1 > productBefore)
|
|
105
|
+
assert.equal(balanceOf(token1, bob), 100_906n)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('rejects expired swaps', async () => {
|
|
109
|
+
await pool.addLiquidity(10_000n, 10_000n)
|
|
110
|
+
await assert.rejects(pool.swapExactTokensForTokens(token0, 1_000n, 1n, alice, Date.now() - 1), {
|
|
111
|
+
message: 'swap expired'
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns the provider share and can restore its state', async () => {
|
|
116
|
+
await pool.addLiquidity(10_000n, 20_000n)
|
|
117
|
+
const restored = new LiquidityPool(token0, token1, 30n, pool.state)
|
|
118
|
+
|
|
119
|
+
assert.deepEqual(restored.reserves, [10_000n, 20_000n])
|
|
120
|
+
assert.equal(restored.liquidityOf(alice), 14_142n)
|
|
121
|
+
|
|
122
|
+
const result = await pool.removeLiquidity(14_142n, 9_999n, 19_999n)
|
|
123
|
+
assert.deepEqual(result, {
|
|
124
|
+
amount0: 10_000n,
|
|
125
|
+
amount1: 20_000n,
|
|
126
|
+
liquidity: 14_142n
|
|
127
|
+
})
|
|
128
|
+
assert.deepEqual(pool.reserves, [0n, 0n])
|
|
129
|
+
assert.equal(pool.totalLiquidity, 0n)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('rejects invalid pairs and missing allowance', async () => {
|
|
133
|
+
assert.throws(() => new LiquidityPool(token0, token0), {
|
|
134
|
+
message: 'pool tokens must differ'
|
|
135
|
+
})
|
|
136
|
+
allowances[token0][alice][poolAddress] = 0n
|
|
137
|
+
await assert.rejects(pool.addLiquidity(10_000n, 10_000n), {
|
|
138
|
+
message: 'amount exceeds allowance'
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('trades mock WETH and USDC through their real token contract interface', async () => {
|
|
143
|
+
const wethAddress = '0xMockWETH'
|
|
144
|
+
const usdcAddress = '0xMockUSDC'
|
|
145
|
+
global.msg = { sender: alice }
|
|
146
|
+
const weth = new MockToken('Mock Wrapped Ether', 'WETH', 18, 100_000n)
|
|
147
|
+
const usdc = new MockToken('Mock USD Coin', 'USDC', 6, 200_000n)
|
|
148
|
+
const tokens = { [wethAddress]: weth, [usdcAddress]: usdc }
|
|
149
|
+
const contractPool = new LiquidityPool(wethAddress, usdcAddress)
|
|
150
|
+
|
|
151
|
+
weth.approve(poolAddress, 100_000n)
|
|
152
|
+
usdc.approve(poolAddress, 200_000n)
|
|
153
|
+
global.msg = {
|
|
154
|
+
sender: alice,
|
|
155
|
+
contract: poolAddress,
|
|
156
|
+
call: async (contract, method, params) => {
|
|
157
|
+
const previousMessage = global.msg
|
|
158
|
+
global.msg = { sender: poolAddress, contract }
|
|
159
|
+
try {
|
|
160
|
+
return await tokens[contract][method](...params)
|
|
161
|
+
} finally {
|
|
162
|
+
global.msg = previousMessage
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await contractPool.addLiquidity(10_000n, 20_000n)
|
|
168
|
+
|
|
169
|
+
assert.deepEqual(contractPool.reserves, [10_000n, 20_000n])
|
|
170
|
+
assert.equal(weth.balanceOf(poolAddress), 10_000n)
|
|
171
|
+
assert.equal(usdc.balanceOf(poolAddress), 20_000n)
|
|
172
|
+
assert.equal(weth.allowance(alice, poolAddress), 90_000n)
|
|
173
|
+
assert.equal(usdc.allowance(alice, poolAddress), 180_000n)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { beforeEach, describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import MockToken from '../exports/mock-token.js'
|
|
4
|
+
|
|
5
|
+
describe('MockToken', () => {
|
|
6
|
+
const owner = '0xOwner'
|
|
7
|
+
const alice = '0xAlice'
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
global.msg = { sender: owner }
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('creates mock WETH, WBNB and USDC with their conventional decimals', () => {
|
|
14
|
+
const assets = [
|
|
15
|
+
new MockToken('Mock Wrapped Ether', 'WETH', 18, 1_000_000n),
|
|
16
|
+
new MockToken('Mock Wrapped BNB', 'WBNB', 18, 2_000_000n),
|
|
17
|
+
new MockToken('Mock USD Coin', 'USDC', 6, 3_000_000n)
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(
|
|
21
|
+
assets.map((asset) => [asset.symbol, asset.decimals, asset.balanceOf(owner)]),
|
|
22
|
+
[
|
|
23
|
+
['WETH', 18, 1_000_000n],
|
|
24
|
+
['WBNB', 18, 2_000_000n],
|
|
25
|
+
['USDC', 6, 3_000_000n]
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('supports transfers, allowances and deterministic state restoration', () => {
|
|
31
|
+
const token = new MockToken('Mock USD Coin', 'USDC', 6, 1_000_000n)
|
|
32
|
+
token.transfer(alice, 100_000n)
|
|
33
|
+
token.approve(alice, 25_000n)
|
|
34
|
+
|
|
35
|
+
const restored = new MockToken('Mock USD Coin', 'USDC', 6, 1_000_000n, [], token.state)
|
|
36
|
+
|
|
37
|
+
assert.equal(restored.balanceOf(owner), 900_000n)
|
|
38
|
+
assert.equal(restored.balanceOf(alice), 100_000n)
|
|
39
|
+
assert.equal(restored.allowance(owner, alice), 25_000n)
|
|
40
|
+
assert.equal(restored.totalSupply, 1_000_000n)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('rejects allocations that do not match the declared supply', () => {
|
|
44
|
+
assert.throws(() => new MockToken('Mock USD Coin', 'USDC', 6, 1_000_000n, [{ address: owner, amount: 1n }]), {
|
|
45
|
+
message: 'allocations must equal initial supply: expected 1000000, got 1'
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
package/test/native-token.js
CHANGED
|
@@ -89,7 +89,7 @@ describe('Leofcoin', () => {
|
|
|
89
89
|
|
|
90
90
|
it('allows allocated holders to transfer', () => {
|
|
91
91
|
msg.sender = receiverAddress
|
|
92
|
-
token.transfer(
|
|
92
|
+
token.transfer(otherReceiverAddress, 10_000n)
|
|
93
93
|
assert.equal(token.balanceOf(receiverAddress), 390_000n)
|
|
94
94
|
assert.equal(token.balanceOf(otherReceiverAddress), 10_000n)
|
|
95
95
|
})
|