@leofcoin/contracts 0.1.22 → 0.1.24
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 +2 -3
- package/src/native-token.ts +124 -7
- package/src/validators.ts +8 -30
- package/test/native-token.js +89 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/contracts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/leofcoin/contracts#readme",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@leofcoin/standards": "^0.3.
|
|
32
|
-
"lucky-numbers": "^1.2.1"
|
|
31
|
+
"@leofcoin/standards": "^0.3.7"
|
|
33
32
|
},
|
|
34
33
|
"devDependencies": {
|
|
35
34
|
"@leofcoin/types": "^1.0.26",
|
package/src/native-token.ts
CHANGED
|
@@ -1,16 +1,133 @@
|
|
|
1
1
|
import Token, { TokenState } from '@leofcoin/standards/token.js'
|
|
2
2
|
|
|
3
|
+
interface GenesisAllocation {
|
|
4
|
+
address: address
|
|
5
|
+
amount: bigint | string
|
|
6
|
+
}
|
|
7
|
+
interface LeofcoinState extends TokenState {
|
|
8
|
+
initialSupply: bigint
|
|
9
|
+
targetSupply: bigint
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MONETARY_POLICY_AUTHORITY = 'leofcoin:protocol:monetary-policy:v1'
|
|
13
|
+
|
|
14
|
+
const genesisState = (initialSupply: bigint, allocations: GenesisAllocation[]): TokenState => {
|
|
15
|
+
initialSupply = BigInt(initialSupply)
|
|
16
|
+
if (initialSupply <= 0n) throw new Error('initial supply must be positive')
|
|
17
|
+
if (!Array.isArray(allocations) || allocations.length === 0)
|
|
18
|
+
throw new Error('genesis allocations are required')
|
|
19
|
+
|
|
20
|
+
const balances: { [address: address]: bigint } = {}
|
|
21
|
+
let allocated = 0n
|
|
22
|
+
for (const allocation of [...allocations].sort((left, right) =>
|
|
23
|
+
left.address.localeCompare(right.address)
|
|
24
|
+
)) {
|
|
25
|
+
if (!allocation.address || balances[allocation.address] !== undefined)
|
|
26
|
+
throw new Error('invalid or duplicate genesis address')
|
|
27
|
+
const amount = BigInt(allocation.amount)
|
|
28
|
+
if (amount <= 0n) throw new Error('genesis allocation must be positive')
|
|
29
|
+
balances[allocation.address] = amount
|
|
30
|
+
allocated += amount
|
|
31
|
+
}
|
|
32
|
+
if (allocated !== initialSupply) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`genesis allocations must equal initial supply: expected ${initialSupply}, got ${allocated}`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
creator: msg.sender,
|
|
40
|
+
createdAt: BigInt(Date.now()),
|
|
41
|
+
roles: {
|
|
42
|
+
OWNER: [msg.sender],
|
|
43
|
+
MINT: [MONETARY_POLICY_AUTHORITY],
|
|
44
|
+
BURN: [MONETARY_POLICY_AUTHORITY]
|
|
45
|
+
},
|
|
46
|
+
holders: BigInt(Object.keys(balances).length),
|
|
47
|
+
balances,
|
|
48
|
+
approvals: {},
|
|
49
|
+
totalSupply: initialSupply
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
3
53
|
export default class Leofcoin extends Token {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
54
|
+
#targetSupply: bigint
|
|
55
|
+
#initialSupply: bigint
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
targetSupply: bigint,
|
|
59
|
+
initialSupplyOrState: bigint | LeofcoinState,
|
|
60
|
+
allocations: GenesisAllocation[] = [],
|
|
61
|
+
restoredState?: LeofcoinState
|
|
62
|
+
) {
|
|
63
|
+
const legacyState =
|
|
64
|
+
initialSupplyOrState &&
|
|
65
|
+
typeof initialSupplyOrState === 'object' &&
|
|
66
|
+
!Array.isArray(initialSupplyOrState)
|
|
67
|
+
? initialSupplyOrState
|
|
68
|
+
: undefined
|
|
69
|
+
const state = restoredState || legacyState
|
|
70
|
+
const initialSupply = state
|
|
71
|
+
? BigInt(state.initialSupply ?? state.totalSupply)
|
|
72
|
+
: BigInt(initialSupplyOrState as bigint)
|
|
73
|
+
super('Leofcoin', 'LFC', 18, state || genesisState(initialSupply, allocations))
|
|
74
|
+
this.#targetSupply = state ? BigInt(state.targetSupply) : BigInt(targetSupply)
|
|
75
|
+
this.#initialSupply = initialSupply
|
|
76
|
+
if (this.#targetSupply <= 0n) throw new Error('target supply must be positive')
|
|
77
|
+
if (this.#initialSupply <= 0n || this.#initialSupply > this.#targetSupply) {
|
|
78
|
+
throw new Error('initial supply must be positive and not exceed target supply')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get targetSupply() {
|
|
83
|
+
return this.#targetSupply
|
|
84
|
+
}
|
|
85
|
+
get initialSupply() {
|
|
86
|
+
return this.#initialSupply
|
|
87
|
+
}
|
|
88
|
+
get name() {
|
|
89
|
+
return 'Leofcoin'
|
|
90
|
+
}
|
|
91
|
+
get symbol() {
|
|
92
|
+
return 'LFC'
|
|
93
|
+
}
|
|
94
|
+
get decimals() {
|
|
95
|
+
return 18
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get state(): LeofcoinState {
|
|
99
|
+
return {
|
|
100
|
+
...super.state,
|
|
101
|
+
initialSupply: this.#initialSupply,
|
|
102
|
+
targetSupply: this.#targetSupply
|
|
103
|
+
} as LeofcoinState
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
grantRole(address: address, role: string) {
|
|
107
|
+
if (role === 'MINT' || role === 'BURN') throw new Error('protocol monetary roles are immutable')
|
|
108
|
+
return super.grantRole(address, role)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
revokeRole(address: address, role: string) {
|
|
112
|
+
if (role === 'MINT' || role === 'BURN') throw new Error('protocol monetary roles are immutable')
|
|
113
|
+
return super.revokeRole(address, role)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
mint(to: address, amount: bigint) {
|
|
117
|
+
if (msg.sender !== MONETARY_POLICY_AUTHORITY)
|
|
118
|
+
throw new Error('only the monetary protocol may mint')
|
|
119
|
+
amount = BigInt(amount)
|
|
120
|
+
if (amount <= 0n) throw new Error('mint amount must be positive')
|
|
121
|
+
if (this.totalSupply + amount > this.#targetSupply)
|
|
122
|
+
throw new Error('mint exceeds target supply')
|
|
123
|
+
return super.mint(to, amount)
|
|
10
124
|
}
|
|
11
125
|
|
|
12
126
|
burn(from: address, amount: bigint) {
|
|
13
|
-
|
|
127
|
+
if (msg.sender !== MONETARY_POLICY_AUTHORITY)
|
|
128
|
+
throw new Error('only the monetary protocol may burn')
|
|
129
|
+
amount = BigInt(amount)
|
|
130
|
+
if (amount <= 0n) throw new Error('burn amount must be positive')
|
|
14
131
|
if (this.balanceOf(from) < amount) throw new Error('amount exceeds balance')
|
|
15
132
|
return super.burn(from, amount)
|
|
16
133
|
}
|
package/src/validators.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { lottery } from 'lucky-numbers'
|
|
2
1
|
import Roles from '@leofcoin/standards/roles.js'
|
|
3
2
|
import type { RolesState } from '@leofcoin/standards/interfaces.js'
|
|
4
3
|
|
|
@@ -142,34 +141,13 @@ export default class Validators extends Roles {
|
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
shuffleValidator() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// add up the bytes
|
|
154
|
-
.map((peer) => {
|
|
155
|
-
peer[1].totalBytes = peer[1].bw.up + peer[1].bw.down
|
|
156
|
-
return peer
|
|
157
|
-
})
|
|
158
|
-
.sort((a, b) => b[1].totalBytes - a[1].totalBytes)
|
|
159
|
-
// only return 128 best participating max
|
|
160
|
-
.splice(0, _peers.length > 128 ? 128 : _peers.length)
|
|
161
|
-
|
|
162
|
-
if (peers.length === 0) return
|
|
163
|
-
|
|
164
|
-
const luckyNumber = peers.length === 1 ? [0] : lottery(1, peers.length - 1)
|
|
165
|
-
|
|
166
|
-
let nextValidator = peers[luckyNumber[0]][0]
|
|
167
|
-
// redraw when the validator is the same
|
|
168
|
-
// but keep the net alive when only one validator is found
|
|
169
|
-
if (this.#currentValidator === nextValidator && peers.length !== 1) {
|
|
170
|
-
const luckyNumber = lottery(1, peers.length - 1)
|
|
171
|
-
nextValidator = peers[luckyNumber[0]][0]
|
|
172
|
-
}
|
|
173
|
-
this.#currentValidator = nextValidator
|
|
144
|
+
const validators = [...new Set(this.#validators)].sort()
|
|
145
|
+
if (validators.length === 0) return
|
|
146
|
+
const seed = `${state.lastBlock?.hash ?? '0x0'}:${state.lastBlock?.index ?? -1}`
|
|
147
|
+
let value = 0n
|
|
148
|
+
for (const character of seed) value = (value * 31n + BigInt(character.charCodeAt(0))) % 4_294_967_291n
|
|
149
|
+
let index = Number(value % BigInt(validators.length))
|
|
150
|
+
if (validators.length > 1 && validators[index] === this.#currentValidator) index = (index + 1) % validators.length
|
|
151
|
+
this.#currentValidator = validators[index]
|
|
174
152
|
}
|
|
175
153
|
}
|
package/test/native-token.js
CHANGED
|
@@ -1,89 +1,130 @@
|
|
|
1
1
|
import { describe, it, beforeEach } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import Leofcoin from './../exports/native-token.js'
|
|
3
|
+
import Leofcoin, { MONETARY_POLICY_AUTHORITY } from './../exports/native-token.js'
|
|
4
4
|
|
|
5
5
|
describe('Leofcoin', () => {
|
|
6
6
|
let token
|
|
7
7
|
const receiverAddress = '0xReceiverAddress'
|
|
8
8
|
const otherReceiverAddress = '0xOtherReceiverAddress'
|
|
9
9
|
const ownerAddress = '0xOwnerAddress'
|
|
10
|
+
const targetSupply = 10_000_000n
|
|
11
|
+
const initialSupply = 1_000_000n
|
|
12
|
+
const allocations = [
|
|
13
|
+
{ address: receiverAddress, amount: 400_000n },
|
|
14
|
+
{ address: ownerAddress, amount: 600_000n }
|
|
15
|
+
]
|
|
10
16
|
|
|
11
17
|
beforeEach(() => {
|
|
12
18
|
global.msg = {
|
|
13
19
|
sender: ownerAddress,
|
|
14
20
|
contract: '0xContractAddress',
|
|
15
|
-
staticCall: async (
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
call: async (currency, method, args) => {
|
|
19
|
-
// Mock implementation of msg.call
|
|
20
|
-
}
|
|
21
|
+
staticCall: async () => {},
|
|
22
|
+
call: async () => {}
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
token = new Leofcoin()
|
|
25
|
+
token = new Leofcoin(targetSupply, initialSupply, allocations)
|
|
24
26
|
})
|
|
25
27
|
|
|
26
|
-
it('
|
|
28
|
+
it('creates the complete initial supply directly from deterministic allocations', () => {
|
|
27
29
|
assert.ok(token instanceof Leofcoin)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should have correct properties', () => {
|
|
31
30
|
assert.equal(token.name, 'Leofcoin')
|
|
32
31
|
assert.equal(token.symbol, 'LFC')
|
|
33
32
|
assert.equal(token.decimals, 18)
|
|
33
|
+
assert.equal(token.targetSupply, targetSupply)
|
|
34
|
+
assert.equal(token.initialSupply, initialSupply)
|
|
35
|
+
assert.equal(token.totalSupply, initialSupply)
|
|
36
|
+
assert.equal(token.balanceOf(ownerAddress), 600_000n)
|
|
37
|
+
assert.equal(token.balanceOf(receiverAddress), 400_000n)
|
|
38
|
+
assert.equal(token.holders, 2n)
|
|
34
39
|
})
|
|
35
40
|
|
|
36
|
-
it('
|
|
37
|
-
|
|
38
|
-
assert.
|
|
41
|
+
it('sorts allocations so equivalent genesis inputs produce identical state', () => {
|
|
42
|
+
const reversed = new Leofcoin(targetSupply, initialSupply, [...allocations].reverse())
|
|
43
|
+
assert.deepEqual(reversed.balances, token.balances)
|
|
39
44
|
})
|
|
40
45
|
|
|
41
|
-
it('
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
it('persists the immutable supplies when restoring state', () => {
|
|
47
|
+
msg.sender = MONETARY_POLICY_AUTHORITY
|
|
48
|
+
token.mint(ownerAddress, 100n)
|
|
49
|
+
const restored = new Leofcoin(999n, token.state)
|
|
50
|
+
assert.equal(restored.targetSupply, targetSupply)
|
|
51
|
+
assert.equal(restored.initialSupply, initialSupply)
|
|
52
|
+
assert.equal(restored.totalSupply, initialSupply + 100n)
|
|
53
|
+
assert.equal(restored.balanceOf(ownerAddress), 600_100n)
|
|
45
54
|
})
|
|
46
55
|
|
|
47
|
-
it('
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
assert.equal(token.
|
|
53
|
-
assert.equal(token.balanceOf(otherReceiverAddress), 10000n)
|
|
56
|
+
it('reserves mint and burn authority for the monetary protocol', () => {
|
|
57
|
+
assert.equal(token.hasRole(ownerAddress, 'OWNER'), true)
|
|
58
|
+
assert.equal(token.hasRole(ownerAddress, 'MINT'), false)
|
|
59
|
+
assert.equal(token.hasRole(ownerAddress, 'BURN'), false)
|
|
60
|
+
assert.equal(token.hasRole(MONETARY_POLICY_AUTHORITY, 'MINT'), true)
|
|
61
|
+
assert.equal(token.hasRole(MONETARY_POLICY_AUTHORITY, 'BURN'), true)
|
|
54
62
|
})
|
|
55
63
|
|
|
56
|
-
it('
|
|
57
|
-
assert.
|
|
64
|
+
it('prevents the owner from minting or burning', () => {
|
|
65
|
+
assert.throws(() => token.mint(ownerAddress, 1n), {
|
|
66
|
+
message: 'only the monetary protocol may mint'
|
|
67
|
+
})
|
|
68
|
+
assert.throws(() => token.burn(ownerAddress, 1n), {
|
|
69
|
+
message: 'only the monetary protocol may burn'
|
|
70
|
+
})
|
|
58
71
|
})
|
|
59
72
|
|
|
60
|
-
it('
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
assert.
|
|
65
|
-
|
|
73
|
+
it('makes protocol monetary roles immutable', () => {
|
|
74
|
+
assert.throws(() => token.grantRole(ownerAddress, 'MINT'), {
|
|
75
|
+
message: 'protocol monetary roles are immutable'
|
|
76
|
+
})
|
|
77
|
+
assert.throws(() => token.revokeRole(MONETARY_POLICY_AUTHORITY, 'BURN'), {
|
|
78
|
+
message: 'protocol monetary roles are immutable'
|
|
79
|
+
})
|
|
66
80
|
})
|
|
67
81
|
|
|
68
|
-
it('
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
it('allows protocol-controlled minting and burning', () => {
|
|
83
|
+
msg.sender = MONETARY_POLICY_AUTHORITY
|
|
84
|
+
token.mint(ownerAddress, 100_000n)
|
|
85
|
+
token.burn(ownerAddress, 10_000n)
|
|
86
|
+
assert.equal(token.balanceOf(ownerAddress), 690_000n)
|
|
87
|
+
assert.equal(token.totalSupply, 1_090_000n)
|
|
88
|
+
})
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
it('allows allocated holders to transfer', () => {
|
|
91
|
+
msg.sender = receiverAddress
|
|
92
|
+
token.transfer(receiverAddress, otherReceiverAddress, 10_000n)
|
|
93
|
+
assert.equal(token.balanceOf(receiverAddress), 390_000n)
|
|
94
|
+
assert.equal(token.balanceOf(otherReceiverAddress), 10_000n)
|
|
75
95
|
})
|
|
76
96
|
|
|
77
|
-
it('
|
|
78
|
-
assert.throws(() =>
|
|
97
|
+
it('rejects invalid genesis allocation sets', () => {
|
|
98
|
+
assert.throws(() => new Leofcoin(targetSupply, initialSupply, []), {
|
|
99
|
+
message: 'genesis allocations are required'
|
|
100
|
+
})
|
|
101
|
+
assert.throws(
|
|
102
|
+
() =>
|
|
103
|
+
new Leofcoin(targetSupply, initialSupply, [
|
|
104
|
+
{ address: ownerAddress, amount: initialSupply - 1n }
|
|
105
|
+
]),
|
|
106
|
+
{ message: /genesis allocations must equal initial supply/ }
|
|
107
|
+
)
|
|
108
|
+
assert.throws(
|
|
109
|
+
() =>
|
|
110
|
+
new Leofcoin(targetSupply, initialSupply, [
|
|
111
|
+
{ address: ownerAddress, amount: 500_000n },
|
|
112
|
+
{ address: ownerAddress, amount: 500_000n }
|
|
113
|
+
]),
|
|
114
|
+
{ message: 'invalid or duplicate genesis address' }
|
|
115
|
+
)
|
|
116
|
+
assert.throws(() => new Leofcoin(1n, 2n, [{ address: ownerAddress, amount: 2n }]), {
|
|
117
|
+
message: 'initial supply must be positive and not exceed target supply'
|
|
118
|
+
})
|
|
79
119
|
})
|
|
80
120
|
|
|
81
|
-
it('
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
message: 'amount exceeds balance'
|
|
121
|
+
it('rejects invalid monetary amounts', () => {
|
|
122
|
+
msg.sender = MONETARY_POLICY_AUTHORITY
|
|
123
|
+
assert.throws(() => token.mint(ownerAddress, 0n), { message: 'mint amount must be positive' })
|
|
124
|
+
assert.throws(() => token.mint(ownerAddress, targetSupply), {
|
|
125
|
+
message: 'mint exceeds target supply'
|
|
87
126
|
})
|
|
127
|
+
assert.throws(() => token.burn(ownerAddress, 0n), { message: 'burn amount must be positive' })
|
|
128
|
+
assert.throws(() => token.burn(ownerAddress, 600_001n), { message: 'amount exceeds balance' })
|
|
88
129
|
})
|
|
89
130
|
})
|