@leofcoin/contracts 0.1.23 → 0.1.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leofcoin/contracts",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,18 +18,18 @@
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://github.com/leofcoin/monorepo"
21
+ "url": "git+https://github.com/leofcoin/monorepo.git",
22
+ "directory": "packages/contracts"
22
23
  },
23
24
  "keywords": [],
24
25
  "author": "",
25
26
  "license": "MIT",
26
27
  "bugs": {
27
- "url": "https://github.com/leofcoin/contracts/issues"
28
+ "url": "https://github.com/leofcoin/monorepo/issues"
28
29
  },
29
- "homepage": "https://github.com/leofcoin/contracts#readme",
30
+ "homepage": "https://github.com/leofcoin/monorepo/tree/main/packages/contracts#readme",
30
31
  "dependencies": {
31
- "@leofcoin/standards": "^0.3.6",
32
- "lucky-numbers": "^1.2.1"
32
+ "@leofcoin/standards": "^0.3.7"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@leofcoin/types": "^1.0.26",
@@ -1,27 +1,133 @@
1
1
  import Token, { TokenState } from '@leofcoin/standards/token.js'
2
2
 
3
- interface LeofcoinState extends TokenState { targetSupply: bigint }
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
+ }
4
52
 
5
53
  export default class Leofcoin extends Token {
6
54
  #targetSupply: bigint
55
+ #initialSupply: bigint
7
56
 
8
- constructor(targetSupply: bigint, state: LeofcoinState) {
9
- super('Leofcoin', 'LFC', 18, state)
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))
10
74
  this.#targetSupply = state ? BigInt(state.targetSupply) : BigInt(targetSupply)
75
+ this.#initialSupply = initialSupply
11
76
  if (this.#targetSupply <= 0n) throw new Error('target supply must be positive')
12
- // The machine executes protocol fee burns as the immutable contract creator.
13
- // Persist this capability in fresh genesis state; restored legacy state is
14
- // intentionally left untouched and cannot activate fee burning.
15
- if (!state && !this.hasRole(msg.sender, 'BURN')) this.grantRole(msg.sender, 'BURN')
16
- if (!state && !this.hasRole(msg.sender, 'MINT')) this.grantRole(msg.sender, 'MINT')
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
17
96
  }
18
97
 
19
- get targetSupply() { return this.#targetSupply }
98
+ get state(): LeofcoinState {
99
+ return {
100
+ ...super.state,
101
+ initialSupply: this.#initialSupply,
102
+ targetSupply: this.#targetSupply
103
+ } as LeofcoinState
104
+ }
20
105
 
21
- get state(): LeofcoinState { return { ...super.state, targetSupply: this.#targetSupply } as LeofcoinState }
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)
124
+ }
22
125
 
23
126
  burn(from: address, amount: bigint) {
24
- // Prevent balance underflow when burning
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')
25
131
  if (this.balanceOf(from) < amount) throw new Error('amount exceeds balance')
26
132
  return super.burn(from, amount)
27
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
 
@@ -127,7 +126,7 @@ export default class Validators extends Roles {
127
126
  if (this.minimumBalance > balance)
128
127
  throw new Error(`balance to low! got: ${balance} need: ${this.#minimumBalance}`)
129
128
 
130
- await msg.call(this.currency, 'transfer', [validator, msg.contract, this.#minimumBalance])
129
+ await msg.call(this.currency, 'transferFrom', [validator, msg.contract, this.#minimumBalance])
131
130
 
132
131
  this.#balances[validator] = this.#minimumBalance
133
132
  this.#validators.push(validator)
@@ -136,40 +135,19 @@ export default class Validators extends Roles {
136
135
  async removeValidator(validator) {
137
136
  this.#isAllowed(validator)
138
137
  if (!this.has(validator)) throw new Error('validator not found')
139
- await msg.call(this.currency, 'transfer', [msg.contract, validator, this.#minimumBalance])
138
+ await msg.call(this.currency, 'transfer', [validator, this.#minimumBalance])
140
139
  delete this.#balances[validator]
141
140
  this.#validators.splice(this.#validators.indexOf(validator), 1)
142
141
  }
143
142
 
144
143
  shuffleValidator() {
145
- // todo introduce voting mechanism
146
- // select amount of peers to vote & pass when 2/3 select the same peer
147
- // this.vote
148
- // todo only ids should be accessable
149
- const _peers = state.peers
150
- const peers = _peers
151
- // only validators make a chance
152
- .filter((peer) => this.#validators.includes(peer[0]))
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/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
  })
@@ -1,92 +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 (currency, method, args) => {
16
- // Mock implementation of msg.staticCall
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(1_000_000n)
25
+ token = new Leofcoin(targetSupply, initialSupply, allocations)
24
26
  })
25
27
 
26
- it('should create an instance of Leofcoin', () => {
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)
34
- assert.equal(token.targetSupply, 1_000_000n)
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)
39
+ })
40
+
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)
35
44
  })
36
45
 
37
- it('should persist the immutable monetary target in state', () => {
46
+ it('persists the immutable supplies when restoring state', () => {
47
+ msg.sender = MONETARY_POLICY_AUTHORITY
38
48
  token.mint(ownerAddress, 100n)
39
49
  const restored = new Leofcoin(999n, token.state)
40
- assert.equal(restored.targetSupply, 1_000_000n)
41
- assert.equal(restored.totalSupply, 100n)
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)
42
54
  })
43
55
 
44
- it('should grant protocol MINT role to owner', async () => {
45
- assert.equal(await token.hasRole(ownerAddress, 'MINT'), true)
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)
46
62
  })
47
63
 
48
- it('should be able to mint', async () => {
49
- await token.mint(ownerAddress, 100000n)
50
- assert.equal(token.balanceOf(ownerAddress), 100000n)
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
+ })
51
71
  })
52
72
 
53
- it('should be able to transfer', async () => {
54
- await token.mint(receiverAddress, 100000n)
55
- msg.sender = receiverAddress
56
- await token.transfer(receiverAddress, otherReceiverAddress, 10000n)
57
- assert.equal(token.balanceOf(receiverAddress), 90000n)
58
- assert.equal(token.balanceOf(otherReceiverAddress), 10000n)
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
+ })
59
80
  })
60
81
 
61
- it('should grant the genesis owner protocol burn authority', () => {
62
- assert.equal(token.hasRole(ownerAddress, 'BURN'), true)
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)
63
88
  })
64
89
 
65
- it('should burn balance and reduce total supply', async () => {
66
- await token.mint(ownerAddress, 100000n)
67
- await token.burn(ownerAddress, 10000n)
68
- assert.equal(token.balanceOf(ownerAddress), 90000n)
69
- assert.equal(token.totalSupply, 90000n)
90
+ it('allows allocated holders to transfer', () => {
91
+ msg.sender = receiverAddress
92
+ token.transfer(otherReceiverAddress, 10_000n)
93
+ assert.equal(token.balanceOf(receiverAddress), 390_000n)
94
+ assert.equal(token.balanceOf(otherReceiverAddress), 10_000n)
70
95
  })
71
96
 
72
- it('should not be able to burn more than balance', async () => {
73
- await token.mint(ownerAddress, 100000n)
74
-
75
- await assert.rejects(async () => await token.burn(ownerAddress, 100001n), {
76
- message: 'amount exceeds balance'
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'
77
118
  })
78
119
  })
79
120
 
80
- it('should not be able to mint if not MINT role', async () => {
81
- msg.sender = receiverAddress
82
- assert.throws(() => token.mint(ownerAddress, 100000n), { message: 'not allowed' })
83
- })
84
-
85
- it('should not be able to transfer more than balance', async () => {
86
- await token.mint(receiverAddress, 100000n)
87
- msg.sender = receiverAddress
88
- assert.throws(() => token.transfer(receiverAddress, otherReceiverAddress, 100001n), {
89
- 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'
90
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' })
91
129
  })
92
130
  })