Skip to content

Commit d8200dd

Browse files
authored
Merge pull request #119 from spacemeshos/fix-unlocked-amount
Calculate available vault balance properly
2 parents 5ad6fef + b2ada61 commit d8200dd

File tree

3 files changed

+169
-10
lines changed

3 files changed

+169
-10
lines changed

src/__tests__/accounts.test.ts

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { getVaultUnlockedAmount } from '../utils/account';
2+
3+
describe('getVaultUnlockedAmount', () => {
4+
describe('Vesting 0-100 [Current: 50]', () => {
5+
const args = {
6+
Owner: 'someOwner',
7+
TotalAmount: '1000000',
8+
VestingStart: 0,
9+
VestingEnd: 100,
10+
InitialUnlockAmount: '250000',
11+
};
12+
it('No incomes / drains', () => {
13+
const r = getVaultUnlockedAmount(args, 50, 1000000n);
14+
expect(r.available).toBe(510000n);
15+
expect(r.totalUnlocked).toBe(510000n);
16+
});
17+
it('With income', () => {
18+
const r = getVaultUnlockedAmount(args, 50, 1000000n + 2000n);
19+
expect(r.available).toBe(512000n); // <-- unlocked + income
20+
expect(r.totalUnlocked).toBe(510000n);
21+
});
22+
it('With drain', () => {
23+
const r = getVaultUnlockedAmount(args, 50, 1000000n - 20000n);
24+
expect(r.available).toBe(490000n); // <-- unlocked - drain
25+
expect(r.totalUnlocked).toBe(510000n);
26+
});
27+
it('With income and drain', () => {
28+
const r = getVaultUnlockedAmount(args, 50, 1000000n - 20000n + 2000n);
29+
expect(r.available).toBe(492000n); // <-- unlocked - drain + income
30+
expect(r.totalUnlocked).toBe(510000n);
31+
});
32+
});
33+
describe('Vesting 100-200 [Current: 39]', () => {
34+
const args = {
35+
Owner: 'someOwner',
36+
TotalAmount: '1000000',
37+
VestingStart: 100,
38+
VestingEnd: 200,
39+
InitialUnlockAmount: '250000',
40+
};
41+
it('No incomes / drains', () => {
42+
const r = getVaultUnlockedAmount(args, 39, 1000000n);
43+
expect(r.available).toBe(0n);
44+
expect(r.totalUnlocked).toBe(0n);
45+
});
46+
it('With income', () => {
47+
const r = getVaultUnlockedAmount(args, 39, 1000000n + 2000n);
48+
expect(r.available).toBe(2000n); // <-- income
49+
expect(r.totalUnlocked).toBe(0n);
50+
});
51+
});
52+
describe('Vesting 100-200 [Current: 127]', () => {
53+
const args = {
54+
Owner: 'someOwner',
55+
TotalAmount: '1000000',
56+
VestingStart: 100,
57+
VestingEnd: 200,
58+
InitialUnlockAmount: '250000',
59+
};
60+
it('No incomes / drains', () => {
61+
const r = getVaultUnlockedAmount(args, 127, 1000000n);
62+
expect(r.available).toBe(280000n);
63+
expect(r.totalUnlocked).toBe(280000n);
64+
});
65+
it('With income', () => {
66+
const r = getVaultUnlockedAmount(args, 127, 1000000n + 2000n);
67+
expect(r.available).toBe(282000n); // <-- unlocked + income
68+
expect(r.totalUnlocked).toBe(280000n);
69+
});
70+
it('With drain', () => {
71+
const r = getVaultUnlockedAmount(args, 127, 1000000n - 20000n);
72+
expect(r.available).toBe(260000n); // <-- unlocked - drain
73+
expect(r.totalUnlocked).toBe(280000n);
74+
});
75+
it('With income and drain', () => {
76+
const r = getVaultUnlockedAmount(args, 127, 1000000n - 20000n + 2000n);
77+
expect(r.available).toBe(262000n); // <-- unlocked - drain + income
78+
expect(r.totalUnlocked).toBe(280000n);
79+
});
80+
});
81+
describe('Rounding: 1 million / 300 layers', () => {
82+
const args = {
83+
Owner: 'someOwner',
84+
TotalAmount: '1000000',
85+
VestingStart: 0,
86+
VestingEnd: 300,
87+
InitialUnlockAmount: '250000',
88+
};
89+
it('Layer 0', () => {
90+
const r = getVaultUnlockedAmount(args, 0, 1000000n);
91+
expect(r.available).toBe(3333n);
92+
expect(r.totalUnlocked).toBe(3333n);
93+
});
94+
it('Layer 1', () => {
95+
const r = getVaultUnlockedAmount(args, 1, 1000000n);
96+
expect(r.available).toBe(6666n);
97+
expect(r.totalUnlocked).toBe(6666n);
98+
});
99+
it('Layer 5', () => {
100+
const r = getVaultUnlockedAmount(args, 5, 1000000n);
101+
expect(r.available).toBe(19998n);
102+
expect(r.totalUnlocked).toBe(19998n);
103+
});
104+
it('Layer 6', () => {
105+
const r = getVaultUnlockedAmount(args, 6, 1000000n);
106+
expect(r.available).toBe(23331n);
107+
expect(r.totalUnlocked).toBe(23331n);
108+
});
109+
it('Layer 100', () => {
110+
const r = getVaultUnlockedAmount(args, 100, 1000000n);
111+
expect(r.available).toBe(336633n);
112+
expect(r.totalUnlocked).toBe(336633n);
113+
});
114+
it('Layer 298', () => {
115+
const r = getVaultUnlockedAmount(args, 298, 1000000n);
116+
expect(r.available).toBe(996567n);
117+
expect(r.totalUnlocked).toBe(996567n);
118+
});
119+
it('Layer 299', () => {
120+
const r = getVaultUnlockedAmount(args, 299, 1000000n);
121+
expect(r.available).toBe(999900n);
122+
expect(r.totalUnlocked).toBe(999900n);
123+
});
124+
it('Layer 300', () => {
125+
const r = getVaultUnlockedAmount(args, 300, 1000000n);
126+
expect(r.available).toBe(1000000n);
127+
expect(r.totalUnlocked).toBe(1000000n);
128+
});
129+
it('Layer 301', () => {
130+
const r = getVaultUnlockedAmount(args, 301, 1000000n);
131+
expect(r.available).toBe(1000000n);
132+
expect(r.totalUnlocked).toBe(1000000n);
133+
});
134+
});
135+
});

src/hooks/useVaultBalance.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ const useVaultBalance = (
4545
O.map(([args, net]) =>
4646
getVaultUnlockedAmount(
4747
args,
48-
layerByTimestamp(net.genesisTime, net.layerDuration, now),
48+
// Calculate unlocked amount for the next layer
49+
layerByTimestamp(net.genesisTime, net.layerDuration, now) + 1,
4950
balance
5051
)
5152
)

src/utils/account.ts

+32-9
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,41 @@ export const extractEligibleKeys = <T extends AnySpawnArguments>(
9393
};
9494

9595
const zeroOrMore = (n: bigint): bigint => (n >= 0n ? n : 0n);
96+
97+
const accumulatedVestedAtLayer = (args: VaultSpawnArguments, layer: number) => {
98+
if (layer < args.VestingStart) return 0n;
99+
if (layer >= args.VestingEnd) return BigInt(args.TotalAmount);
100+
101+
const vestingPeriod = BigInt(args.VestingEnd) - BigInt(args.VestingStart);
102+
const layersPassed = BigInt(layer) - BigInt(args.VestingStart) + 1n;
103+
const vestedLayers =
104+
layersPassed < vestingPeriod ? zeroOrMore(layersPassed) : vestingPeriod;
105+
const vestedPerLayer = BigInt(args.TotalAmount) / vestingPeriod;
106+
return vestedLayers * vestedPerLayer;
107+
};
108+
109+
const vestAtLayer = (args: VaultSpawnArguments, layer: number) => {
110+
if (layer < BigInt(args.VestingStart) || layer > BigInt(args.VestingEnd))
111+
return 0n;
112+
113+
const prevLayerVested = accumulatedVestedAtLayer(args, layer - 1);
114+
const curLayerVested = accumulatedVestedAtLayer(args, layer);
115+
return curLayerVested - prevLayerVested;
116+
};
117+
118+
const unlockedAtLayer = (args: VaultSpawnArguments, layer: number) => {
119+
const acc = accumulatedVestedAtLayer(args, layer - 1);
120+
const vest = vestAtLayer(args, layer);
121+
return acc + vest;
122+
};
123+
96124
export const getVaultUnlockedAmount = (
97125
args: VaultSpawnArguments,
98-
currentLayer: number,
99-
currentBalance: bigint
126+
layer: number,
127+
balanceAtLayer: bigint
100128
) => {
101-
const vestingPeriod = BigInt(args.VestingEnd) - BigInt(args.VestingStart);
102-
const layersPassed = BigInt(currentLayer) - BigInt(args.VestingStart);
103-
const vestedLayers =
104-
layersPassed < vestingPeriod ? layersPassed : vestingPeriod;
105-
const vestingPerLayer = BigInt(args.TotalAmount) / vestingPeriod;
106-
const totalUnlocked = vestedLayers * vestingPerLayer;
107-
const alreadySpent = BigInt(args.TotalAmount) - currentBalance;
129+
const totalUnlocked = unlockedAtLayer(args, layer);
130+
const alreadySpent = BigInt(args.TotalAmount) - balanceAtLayer;
108131
const available = totalUnlocked - alreadySpent;
109132
return {
110133
totalUnlocked: zeroOrMore(totalUnlocked),

0 commit comments

Comments
 (0)