Skip to content

Commit 31a7b90

Browse files
committed
burn alpha precompile
1 parent 62e433a commit 31a7b90

File tree

5 files changed

+284
-16
lines changed

5 files changed

+284
-16
lines changed

evm-tests/src/contracts/staking.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,5 +407,28 @@ export const IStakingV2ABI = [
407407
"outputs": [],
408408
"stateMutability": "nonpayable",
409409
"type": "function"
410+
},
411+
{
412+
"inputs": [
413+
{
414+
"internalType": "bytes32",
415+
"name": "hotkey",
416+
"type": "bytes32"
417+
},
418+
{
419+
"internalType": "uint256",
420+
"name": "amount",
421+
"type": "uint256"
422+
},
423+
{
424+
"internalType": "uint256",
425+
"name": "netuid",
426+
"type": "uint256"
427+
}
428+
],
429+
"name": "burnAlpha",
430+
"outputs": [],
431+
"stateMutability": "nonpayable",
432+
"type": "function"
410433
}
411434
];
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import * as assert from "assert";
2+
import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate"
3+
import { devnet } from "@polkadot-api/descriptors"
4+
import { PolkadotSigner, TypedApi } from "polkadot-api";
5+
import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils"
6+
import { tao } from "../src/balance-math"
7+
import { ethers } from "ethers"
8+
import { generateRandomEthersWallet, getPublicClient } from "../src/utils"
9+
import { convertH160ToPublicKey } from "../src/address-utils"
10+
import {
11+
forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister,
12+
startCall,
13+
} from "../src/subtensor"
14+
import { ETH_LOCAL_URL } from "../src/config";
15+
import { ISTAKING_V2_ADDRESS, IStakingV2ABI } from "../src/contracts/staking"
16+
import { PublicClient } from "viem";
17+
18+
describe("Test staking precompile burn alpha", () => {
19+
// init eth part
20+
const wallet1 = generateRandomEthersWallet();
21+
let publicClient: PublicClient;
22+
// init substrate part
23+
const hotkey = getRandomSubstrateKeypair();
24+
const coldkey = getRandomSubstrateKeypair();
25+
26+
let api: TypedApi<typeof devnet>
27+
28+
before(async () => {
29+
publicClient = await getPublicClient(ETH_LOCAL_URL)
30+
// init variables got from await and async
31+
api = await getDevnetApi()
32+
33+
await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey))
34+
await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey))
35+
await forceSetBalanceToEthAddress(api, wallet1.address)
36+
37+
let netuid = await addNewSubnetwork(api, hotkey, coldkey)
38+
await startCall(api, netuid, coldkey)
39+
40+
console.log("test the case on subnet ", netuid)
41+
42+
await burnedRegister(api, netuid, convertH160ToSS58(wallet1.address), coldkey)
43+
})
44+
45+
it("Can burn alpha after adding stake", async () => {
46+
let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1
47+
48+
// First add some stake
49+
let stakeBalance = tao(50)
50+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
51+
const addStakeTx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), netuid)
52+
await addStakeTx.wait()
53+
54+
// Get stake before burning
55+
const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(
56+
convertPublicKeyToSs58(hotkey.publicKey),
57+
convertH160ToSS58(wallet1.address),
58+
netuid
59+
)
60+
61+
console.log("Stake before burn:", stakeBefore)
62+
assert.ok(stakeBefore > BigInt(0), "Should have stake before burning")
63+
64+
// Burn some alpha (burn 20 TAO worth)
65+
let burnAmount = tao(20)
66+
const burnTx = await contract.burnAlpha(hotkey.publicKey, burnAmount.toString(), netuid)
67+
await burnTx.wait()
68+
69+
// Get stake after burning
70+
const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(
71+
convertPublicKeyToSs58(hotkey.publicKey),
72+
convertH160ToSS58(wallet1.address),
73+
netuid
74+
)
75+
76+
console.log("Stake after burn:", stakeAfter)
77+
78+
// Verify that stake decreased by burn amount
79+
assert.ok(stakeAfter < stakeBefore, "Stake should decrease after burning")
80+
assert.strictEqual(stakeBefore - stakeAfter, burnAmount, "Stake should decrease by exactly burn amount")
81+
})
82+
83+
it("Cannot burn more alpha than staked", async () => {
84+
let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1
85+
86+
// Get current stake
87+
const currentStake = await api.query.SubtensorModule.Alpha.getValue(
88+
convertPublicKeyToSs58(hotkey.publicKey),
89+
convertH160ToSS58(wallet1.address),
90+
netuid
91+
)
92+
93+
// Try to burn more than staked
94+
let burnAmount = currentStake + tao(100)
95+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
96+
97+
try {
98+
const burnTx = await contract.burnAlpha(hotkey.publicKey, burnAmount.toString(), netuid)
99+
await burnTx.wait()
100+
assert.fail("Transaction should have failed - cannot burn more than staked");
101+
} catch (error) {
102+
// Transaction failed as expected
103+
console.log("Correctly failed to burn more than staked amount")
104+
assert.ok(true, "Burning more than staked should fail");
105+
}
106+
})
107+
108+
it("Cannot burn alpha from non-existent subnet", async () => {
109+
// wrong netuid
110+
let netuid = 12345;
111+
let burnAmount = tao(10)
112+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
113+
114+
try {
115+
const burnTx = await contract.burnAlpha(hotkey.publicKey, burnAmount.toString(), netuid)
116+
await burnTx.wait()
117+
assert.fail("Transaction should have failed - subnet doesn't exist");
118+
} catch (error) {
119+
// Transaction failed as expected
120+
console.log("Correctly failed to burn from non-existent subnet")
121+
assert.ok(true, "Burning from non-existent subnet should fail");
122+
}
123+
})
124+
125+
it("Can burn all remaining alpha", async () => {
126+
let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1
127+
128+
// Get current stake
129+
const currentStake = await api.query.SubtensorModule.Alpha.getValue(
130+
convertPublicKeyToSs58(hotkey.publicKey),
131+
convertH160ToSS58(wallet1.address),
132+
netuid
133+
)
134+
135+
console.log("Current stake before burning all:", currentStake)
136+
assert.ok(currentStake > BigInt(0), "Should have stake before burning all")
137+
138+
// Burn all remaining alpha
139+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
140+
const burnTx = await contract.burnAlpha(hotkey.publicKey, currentStake.toString(), netuid)
141+
await burnTx.wait()
142+
143+
// Get stake after burning all
144+
const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(
145+
convertPublicKeyToSs58(hotkey.publicKey),
146+
convertH160ToSS58(wallet1.address),
147+
netuid
148+
)
149+
150+
console.log("Stake after burning all:", stakeAfter)
151+
152+
// Verify that stake is now zero
153+
assert.strictEqual(stakeAfter, BigInt(0), "Stake should be zero after burning all")
154+
})
155+
156+
it("Cannot burn zero alpha", async () => {
157+
let netuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1
158+
159+
// First add some stake for this test
160+
let stakeBalance = tao(10)
161+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
162+
const addStakeTx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), netuid)
163+
await addStakeTx.wait()
164+
165+
// Try to burn zero amount
166+
let burnAmount = BigInt(0)
167+
168+
try {
169+
const burnTx = await contract.burnAlpha(hotkey.publicKey, burnAmount.toString(), netuid)
170+
await burnTx.wait()
171+
assert.fail("Transaction should have failed - cannot burn zero amount");
172+
} catch (error) {
173+
// Transaction failed as expected
174+
console.log("Correctly failed to burn zero amount")
175+
assert.ok(true, "Burning zero amount should fail");
176+
}
177+
})
178+
})
179+

precompiles/src/solidity/stakingV2.abi

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
],
1010
"name": "addProxy",
1111
"outputs": [],
12-
"stateMutability": "nonpayable",
12+
"stateMutability": "payable",
1313
"type": "function"
1414
},
1515
{
@@ -226,7 +226,7 @@
226226
],
227227
"name": "moveStake",
228228
"outputs": [],
229-
"stateMutability": "nonpayable",
229+
"stateMutability": "payable",
230230
"type": "function"
231231
},
232232
{
@@ -239,7 +239,7 @@
239239
],
240240
"name": "removeProxy",
241241
"outputs": [],
242-
"stateMutability": "nonpayable",
242+
"stateMutability": "payable",
243243
"type": "function"
244244
},
245245
{
@@ -262,7 +262,7 @@
262262
],
263263
"name": "removeStake",
264264
"outputs": [],
265-
"stateMutability": "nonpayable",
265+
"stateMutability": "payable",
266266
"type": "function"
267267
},
268268
{
@@ -280,7 +280,7 @@
280280
],
281281
"name": "removeStakeFull",
282282
"outputs": [],
283-
"stateMutability": "nonpayable",
283+
"stateMutability": "payable",
284284
"type": "function"
285285
},
286286
{
@@ -303,7 +303,7 @@
303303
],
304304
"name": "removeStakeFullLimit",
305305
"outputs": [],
306-
"stateMutability": "nonpayable",
306+
"stateMutability": "payable",
307307
"type": "function"
308308
},
309309
{
@@ -336,7 +336,7 @@
336336
],
337337
"name": "removeStakeLimit",
338338
"outputs": [],
339-
"stateMutability": "nonpayable",
339+
"stateMutability": "payable",
340340
"type": "function"
341341
},
342342
{
@@ -369,7 +369,30 @@
369369
],
370370
"name": "transferStake",
371371
"outputs": [],
372-
"stateMutability": "nonpayable",
372+
"stateMutability": "payable",
373+
"type": "function"
374+
},
375+
{
376+
"inputs": [
377+
{
378+
"internalType": "bytes32",
379+
"name": "hotkey",
380+
"type": "bytes32"
381+
},
382+
{
383+
"internalType": "uint256",
384+
"name": "amount",
385+
"type": "uint256"
386+
},
387+
{
388+
"internalType": "uint256",
389+
"name": "netuid",
390+
"type": "uint256"
391+
}
392+
],
393+
"name": "burnAlpha",
394+
"outputs": [],
395+
"stateMutability": "payable",
373396
"type": "function"
374397
}
375398
]

precompiles/src/solidity/stakingV2.sol

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ interface IStaking {
4848
bytes32 hotkey,
4949
uint256 amount,
5050
uint256 netuid
51-
) external;
51+
) external payable;
5252

5353
/**
5454
* @dev Moves a subtensor stake `amount` associated with the `hotkey` to a different hotkey
@@ -76,7 +76,7 @@ interface IStaking {
7676
uint256 origin_netuid,
7777
uint256 destination_netuid,
7878
uint256 amount
79-
) external;
79+
) external payable;
8080

8181
/**
8282
* @dev Transfer a subtensor stake `amount` associated with the transaction signer to a different coldkey
@@ -104,7 +104,7 @@ interface IStaking {
104104
uint256 origin_netuid,
105105
uint256 destination_netuid,
106106
uint256 amount
107-
) external;
107+
) external payable;
108108

109109
/**
110110
* @dev Returns the amount of RAO staked by the coldkey.
@@ -156,14 +156,14 @@ interface IStaking {
156156
*
157157
* @param delegate The public key (32 bytes) of the delegate.
158158
*/
159-
function addProxy(bytes32 delegate) external;
159+
function addProxy(bytes32 delegate) external payable;
160160

161161
/**
162162
* @dev Removes staking proxy account.
163163
*
164164
* @param delegate The public key (32 bytes) of the delegate.
165165
*/
166-
function removeProxy(bytes32 delegate) external;
166+
function removeProxy(bytes32 delegate) external payable;
167167

168168
/**
169169
* @dev Returns the validators that have staked alpha under a hotkey.
@@ -258,7 +258,7 @@ interface IStaking {
258258
uint256 limit_price,
259259
bool allow_partial,
260260
uint256 netuid
261-
) external;
261+
) external payable;
262262

263263
/**
264264
* @dev Removes all stake from a hotkey on a subnet with a price limit.
@@ -270,7 +270,7 @@ interface IStaking {
270270
* @param hotkey The hotkey public key (32 bytes).
271271
* @param netuid The subnet to remove stake from (uint256).
272272
*/
273-
function removeStakeFull(bytes32 hotkey, uint256 netuid) external;
273+
function removeStakeFull(bytes32 hotkey, uint256 netuid) external payable;
274274

275275
/**
276276
* @dev Removes all stake from a hotkey on a subnet with a price limit.
@@ -287,5 +287,27 @@ interface IStaking {
287287
bytes32 hotkey,
288288
uint256 netuid,
289289
uint256 limitPrice
290-
) external;
290+
) external payable;
291+
292+
/**
293+
* @dev Burns alpha tokens from the specified hotkey's stake on a subnet.
294+
*
295+
* This function allows external accounts and contracts to permanently burn (destroy) alpha tokens
296+
* from their stake on a specified hotkey and subnet. The burned tokens are removed from circulation
297+
* and cannot be recovered.
298+
*
299+
* @param hotkey The hotkey public key (32 bytes).
300+
* @param amount The amount of alpha to burn (uint256).
301+
* @param netuid The subnet to burn from (uint256).
302+
*
303+
* Requirements:
304+
* - `hotkey` must be a valid hotkey registered on the network.
305+
* - The caller must have sufficient alpha staked to the specified hotkey on the subnet.
306+
* - `amount` must be greater than zero and not exceed the staked amount.
307+
*/
308+
function burnAlpha(
309+
bytes32 hotkey,
310+
uint256 amount,
311+
uint256 netuid
312+
) external payable;
291313
}

0 commit comments

Comments
 (0)