Skip to content

Commit 1ce8574

Browse files
authored
Merge pull request #2478 from jeremyp-tao/jeremy-staking-precompile-approvals
Add `transferStakeFrom` and `approve` functions to staking precompile
2 parents 9f75b9d + 869baac commit 1ce8574

File tree

5 files changed

+823
-3
lines changed

5 files changed

+823
-3
lines changed

contract-tests/src/contracts/staking.ts

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,5 +454,141 @@ export const IStakingV2ABI = [
454454
"outputs": [],
455455
"stateMutability": "payable",
456456
"type": "function"
457+
},
458+
{
459+
"inputs": [
460+
{
461+
"internalType": "address",
462+
"name": "spenderAddress",
463+
"type": "address"
464+
},
465+
{
466+
"internalType": "uint256",
467+
"name": "netuid",
468+
"type": "uint256"
469+
},
470+
{
471+
"internalType": "uint256",
472+
"name": "absoluteAmount",
473+
"type": "uint256"
474+
}
475+
],
476+
"name": "approve",
477+
"outputs": [],
478+
"stateMutability": "",
479+
"type": "function"
480+
},
481+
{
482+
"inputs": [
483+
{
484+
"internalType": "address",
485+
"name": "sourceAddress",
486+
"type": "address"
487+
},
488+
{
489+
"internalType": "address",
490+
"name": "spenderAddress",
491+
"type": "address"
492+
},
493+
{
494+
"internalType": "uint256",
495+
"name": "netuid",
496+
"type": "uint256"
497+
}
498+
],
499+
"name": "allowance",
500+
"outputs": [
501+
{
502+
"internalType": "uint256",
503+
"name": "",
504+
"type": "uint256"
505+
}
506+
],
507+
"stateMutability": "view",
508+
"type": "function"
509+
},
510+
{
511+
"inputs": [
512+
{
513+
"internalType": "address",
514+
"name": "spenderAddress",
515+
"type": "address"
516+
},
517+
{
518+
"internalType": "uint256",
519+
"name": "netuid",
520+
"type": "uint256"
521+
},
522+
{
523+
"internalType": "uint256",
524+
"name": "increaseAmount",
525+
"type": "uint256"
526+
}
527+
],
528+
"name": "increaseAllowance",
529+
"outputs": [],
530+
"stateMutability": "",
531+
"type": "function"
532+
},
533+
{
534+
"inputs": [
535+
{
536+
"internalType": "address",
537+
"name": "spenderAddress",
538+
"type": "address"
539+
},
540+
{
541+
"internalType": "uint256",
542+
"name": "netuid",
543+
"type": "uint256"
544+
},
545+
{
546+
"internalType": "uint256",
547+
"name": "decreaseAmount",
548+
"type": "uint256"
549+
}
550+
],
551+
"name": "decreaseAllowance",
552+
"outputs": [],
553+
"stateMutability": "",
554+
"type": "function"
555+
},
556+
{
557+
"inputs": [
558+
{
559+
"internalType": "address",
560+
"name": "source_address",
561+
"type": "address"
562+
},
563+
{
564+
"internalType": "bytes32",
565+
"name": "destination_coldkey",
566+
"type": "bytes32"
567+
},
568+
{
569+
"internalType": "bytes32",
570+
"name": "hotkey",
571+
"type": "bytes32"
572+
},
573+
{
574+
"internalType": "uint256",
575+
"name": "origin_netuid",
576+
"type": "uint256"
577+
},
578+
{
579+
"internalType": "uint256",
580+
"name": "destination_netuid",
581+
"type": "uint256"
582+
},
583+
{
584+
"internalType": "uint256",
585+
"name": "amount",
586+
"type": "uint256"
587+
}
588+
],
589+
"name": "transferStakeFrom",
590+
"outputs": [],
591+
"stateMutability": "",
592+
"type": "function"
457593
}
458-
];
594+
];
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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 { raoToEth, 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+
sendProxyCall,
13+
startCall,
14+
} from "../src/subtensor"
15+
import { ETH_LOCAL_URL } from "../src/config";
16+
import { ISTAKING_ADDRESS, ISTAKING_V2_ADDRESS, IStakingABI, IStakingV2ABI } from "../src/contracts/staking"
17+
import { PublicClient } from "viem";
18+
19+
describe("Test approval in staking precompile", () => {
20+
// init eth part
21+
const wallet1 = generateRandomEthersWallet();
22+
const wallet2 = generateRandomEthersWallet();
23+
let publicClient: PublicClient;
24+
// init substrate part
25+
const hotkey = getRandomSubstrateKeypair();
26+
const coldkey = getRandomSubstrateKeypair();
27+
const proxy = getRandomSubstrateKeypair();
28+
29+
let api: TypedApi<typeof devnet>
30+
let stakeNetuid: number;
31+
32+
let expectedAllowance = BigInt(0);
33+
34+
// sudo account alice as signer
35+
let alice: PolkadotSigner;
36+
before(async () => {
37+
publicClient = await getPublicClient(ETH_LOCAL_URL)
38+
// init variables got from await and async
39+
api = await getDevnetApi()
40+
41+
// await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey))
42+
await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey))
43+
await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey))
44+
await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(proxy.publicKey))
45+
await forceSetBalanceToEthAddress(api, wallet1.address)
46+
await forceSetBalanceToEthAddress(api, wallet2.address)
47+
let netuid = await addNewSubnetwork(api, hotkey, coldkey)
48+
await startCall(api, netuid, coldkey)
49+
50+
console.log("test the case on subnet ", netuid)
51+
52+
await burnedRegister(api, netuid, convertH160ToSS58(wallet1.address), coldkey)
53+
await burnedRegister(api, netuid, convertH160ToSS58(wallet2.address), coldkey)
54+
55+
// add stake as wallet1
56+
{
57+
stakeNetuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1
58+
// the unit in V2 is RAO, not ETH
59+
let stakeBalance = tao(20)
60+
const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid)
61+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
62+
const tx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), stakeNetuid)
63+
await tx.wait()
64+
65+
const stakeFromContract = BigInt(
66+
await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), stakeNetuid)
67+
);
68+
69+
assert.ok(stakeFromContract > stakeBefore)
70+
const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid)
71+
assert.ok(stakeAfter > stakeBefore)
72+
}
73+
})
74+
75+
it("Can't transfer from account without approval", async () => {
76+
try {
77+
// wallet2 tries to transfer from wallet1
78+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2);
79+
const tx = await contract.transferStakeFrom(
80+
wallet1.address, // source
81+
convertH160ToPublicKey(wallet2.address), // distination
82+
hotkey.publicKey,
83+
stakeNetuid,
84+
stakeNetuid,
85+
1
86+
)
87+
await tx.wait();
88+
89+
assert.fail("should have reverted due to missing allowance");
90+
} catch (e) {
91+
assert.equal(e.reason, "trying to spend more than allowed", "wrong revert message");
92+
}
93+
})
94+
95+
it("Can approve some amount", async () => {
96+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
97+
98+
{
99+
let allowance = BigInt(
100+
await contract.allowance(
101+
wallet1.address, // source
102+
wallet2.address, // spender
103+
stakeNetuid,
104+
)
105+
);
106+
assert.equal(allowance, expectedAllowance, "default allowance should be 0");
107+
}
108+
109+
{
110+
const tx = await contract.approve(
111+
wallet2.address, // spender
112+
stakeNetuid,
113+
tao(10)
114+
)
115+
await tx.wait();
116+
117+
expectedAllowance += BigInt(tao(10));
118+
119+
let allowance = BigInt(
120+
await contract.allowance(
121+
wallet1.address, // source
122+
wallet2.address, // spender
123+
stakeNetuid,
124+
)
125+
);
126+
assert.equal(allowance, expectedAllowance, "should have set allowance");
127+
}
128+
})
129+
130+
it("Can now use transfer from", async () => {
131+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2);
132+
133+
// wallet2 transfer from wallet1
134+
const tx = await contract.transferStakeFrom(
135+
wallet1.address, // source
136+
convertH160ToPublicKey(wallet2.address), // destination
137+
hotkey.publicKey,
138+
stakeNetuid,
139+
stakeNetuid,
140+
tao(5)
141+
)
142+
await tx.wait();
143+
144+
expectedAllowance -= BigInt(tao(5));
145+
146+
{
147+
let allowance = BigInt(
148+
await contract.allowance(
149+
wallet1.address, // source
150+
wallet2.address, // spender
151+
stakeNetuid,
152+
)
153+
);
154+
assert.equal(allowance, expectedAllowance, "allowance should now be 500");
155+
}
156+
})
157+
158+
it("Can't use transfer from with amount too high", async () => {
159+
try {
160+
// wallet2 tries to transfer from wallet1
161+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2);
162+
const tx = await contract.transferStakeFrom(
163+
wallet1.address, // source
164+
convertH160ToPublicKey(wallet2.address), // distination
165+
hotkey.publicKey,
166+
stakeNetuid,
167+
stakeNetuid,
168+
expectedAllowance + BigInt(1)
169+
)
170+
await tx.wait();
171+
172+
assert.fail("should have reverted due to missing allowance");
173+
} catch (e) {
174+
assert.equal(e.reason, "trying to spend more than allowed", "wrong revert message");
175+
}
176+
})
177+
178+
it("Approval functions works as expected", async () => {
179+
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
180+
181+
{
182+
const tx = await contract.increaseAllowance(
183+
wallet2.address, // spender
184+
stakeNetuid,
185+
tao(10)
186+
)
187+
await tx.wait();
188+
189+
expectedAllowance += BigInt(tao(10));
190+
191+
let allowance = BigInt(
192+
await contract.allowance(
193+
wallet1.address, // source
194+
wallet2.address, // spender
195+
stakeNetuid,
196+
)
197+
);
198+
assert.equal(allowance, expectedAllowance, "allowance have been increased correctly");
199+
}
200+
201+
{
202+
const tx = await contract.decreaseAllowance(
203+
wallet2.address, // spender
204+
stakeNetuid,
205+
tao(2)
206+
)
207+
await tx.wait();
208+
209+
expectedAllowance -= BigInt(tao(2));
210+
211+
let allowance = BigInt(
212+
await contract.allowance(
213+
wallet1.address, // source
214+
wallet2.address, // spender
215+
stakeNetuid,
216+
)
217+
);
218+
assert.equal(allowance, expectedAllowance, "allowance have been decreased correctly");
219+
}
220+
221+
{
222+
const tx = await contract.approve(
223+
wallet2.address, // spender
224+
stakeNetuid,
225+
0
226+
)
227+
await tx.wait();
228+
229+
expectedAllowance = BigInt(0);
230+
231+
let allowance = BigInt(
232+
await contract.allowance(
233+
wallet1.address, // source
234+
wallet2.address, // spender
235+
stakeNetuid,
236+
)
237+
);
238+
assert.equal(allowance, expectedAllowance, "allowance have been overwritten correctly");
239+
}
240+
})
241+
})

0 commit comments

Comments
 (0)