Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 137 additions & 1 deletion contract-tests/src/contracts/staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,5 +454,141 @@ export const IStakingV2ABI = [
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "spenderColdkey",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "netuid",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "absoluteAmount",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "sourceColdkey",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "spenderColdkey",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "netuid",
"type": "uint256"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "spenderColdkey",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "netuid",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "increaseAmount",
"type": "uint256"
}
],
"name": "increaseAllowance",
"outputs": [],
"stateMutability": "",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "spenderColdkey",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "netuid",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "decreaseAmount",
"type": "uint256"
}
],
"name": "decreaseAllowance",
"outputs": [],
"stateMutability": "",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "source_coldkey",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "destination_coldkey",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "hotkey",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "origin_netuid",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "destination_netuid",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "transferStakeFrom",
"outputs": [],
"stateMutability": "",
"type": "function"
}
];
];
241 changes: 241 additions & 0 deletions contract-tests/test/staking.precompile.approval.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import * as assert from "assert";
import { getDevnetApi, getRandomSubstrateKeypair } from "../src/substrate"
import { devnet } from "@polkadot-api/descriptors"
import { PolkadotSigner, TypedApi } from "polkadot-api";
import { convertPublicKeyToSs58, convertH160ToSS58 } from "../src/address-utils"
import { raoToEth, tao } from "../src/balance-math"
import { ethers } from "ethers"
import { generateRandomEthersWallet, getPublicClient } from "../src/utils"
import { convertH160ToPublicKey } from "../src/address-utils"
import {
forceSetBalanceToEthAddress, forceSetBalanceToSs58Address, addNewSubnetwork, burnedRegister,
sendProxyCall,
startCall,
} from "../src/subtensor"
import { ETH_LOCAL_URL } from "../src/config";
import { ISTAKING_ADDRESS, ISTAKING_V2_ADDRESS, IStakingABI, IStakingV2ABI } from "../src/contracts/staking"
import { PublicClient } from "viem";

describe("Test approval in staking precompile", () => {
// init eth part
const wallet1 = generateRandomEthersWallet();
const wallet2 = generateRandomEthersWallet();
let publicClient: PublicClient;
// init substrate part
const hotkey = getRandomSubstrateKeypair();
const coldkey = getRandomSubstrateKeypair();
const proxy = getRandomSubstrateKeypair();

let api: TypedApi<typeof devnet>
let stakeNetuid: number;

let expectedAllowance = BigInt(0);

// sudo account alice as signer
let alice: PolkadotSigner;
before(async () => {
publicClient = await getPublicClient(ETH_LOCAL_URL)
// init variables got from await and async
api = await getDevnetApi()

// await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(alice.publicKey))
await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(hotkey.publicKey))
await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(coldkey.publicKey))
await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(proxy.publicKey))
await forceSetBalanceToEthAddress(api, wallet1.address)
await forceSetBalanceToEthAddress(api, wallet2.address)
let netuid = await addNewSubnetwork(api, hotkey, coldkey)
await startCall(api, netuid, coldkey)

console.log("test the case on subnet ", netuid)

await burnedRegister(api, netuid, convertH160ToSS58(wallet1.address), coldkey)
await burnedRegister(api, netuid, convertH160ToSS58(wallet2.address), coldkey)

// add stake as wallet1
{
stakeNetuid = (await api.query.SubtensorModule.TotalNetworks.getValue()) - 1
// the unit in V2 is RAO, not ETH
let stakeBalance = tao(20)
const stakeBefore = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid)
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);
const tx = await contract.addStake(hotkey.publicKey, stakeBalance.toString(), stakeNetuid)
await tx.wait()

const stakeFromContract = BigInt(
await contract.getStake(hotkey.publicKey, convertH160ToPublicKey(wallet1.address), stakeNetuid)
);

assert.ok(stakeFromContract > stakeBefore)
const stakeAfter = await api.query.SubtensorModule.Alpha.getValue(convertPublicKeyToSs58(hotkey.publicKey), convertH160ToSS58(wallet1.address), stakeNetuid)
assert.ok(stakeAfter > stakeBefore)
}
})

it("Can't transfer from account without approval", async () => {
try {
// wallet2 tries to transfer from wallet1
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2);
const tx = await contract.transferStakeFrom(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // distination
hotkey.publicKey,
stakeNetuid,
stakeNetuid,
1
)
await tx.wait();

assert.fail("should have reverted due to missing allowance");
} catch (e) {
assert.equal(e.reason, "trying to spend more than allowed", "wrong revert message");
}
})

it("Can approve some amount", async () => {
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);

{
let allowance = BigInt(
await contract.allowance(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
)
);
assert.equal(allowance, expectedAllowance, "default allowance should be 0");
}

{
const tx = await contract.approve(
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
tao(10)
)
await tx.wait();

expectedAllowance += BigInt(tao(10));

let allowance = BigInt(
await contract.allowance(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
)
);
assert.equal(allowance, expectedAllowance, "should have set allowance");
}
})

it("Can now use transfer from", async () => {
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2);

// wallet2 transfer from wallet1
const tx = await contract.transferStakeFrom(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // distination
hotkey.publicKey,
stakeNetuid,
stakeNetuid,
tao(5)
)
await tx.wait();

expectedAllowance -= BigInt(tao(5));

{
let allowance = BigInt(
await contract.allowance(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
)
);
assert.equal(allowance, expectedAllowance, "allowance should now be 500");
}
})

it("Can't use transfer from with amount too high", async () => {
try {
// wallet2 tries to transfer from wallet1
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet2);
const tx = await contract.transferStakeFrom(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // distination
hotkey.publicKey,
stakeNetuid,
stakeNetuid,
expectedAllowance + BigInt(1)
)
await tx.wait();

assert.fail("should have reverted due to missing allowance");
} catch (e) {
assert.equal(e.reason, "trying to spend more than allowed", "wrong revert message");
}
})

it("Approval functions works as expected", async () => {
const contract = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, wallet1);

{
const tx = await contract.increaseAllowance(
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
tao(10)
)
await tx.wait();

expectedAllowance += BigInt(tao(10));

let allowance = BigInt(
await contract.allowance(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
)
);
assert.equal(allowance, expectedAllowance, "allowance have been increased correctly");
}

{
const tx = await contract.decreaseAllowance(
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
tao(2)
)
await tx.wait();

expectedAllowance -= BigInt(tao(2));

let allowance = BigInt(
await contract.allowance(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
)
);
assert.equal(allowance, expectedAllowance, "allowance have been decreased correctly");
}

{
const tx = await contract.approve(
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
0
)
await tx.wait();

expectedAllowance = BigInt(0);

let allowance = BigInt(
await contract.allowance(
convertH160ToPublicKey(wallet1.address), // source
convertH160ToPublicKey(wallet2.address), // destination
stakeNetuid,
)
);
assert.equal(allowance, expectedAllowance, "allowance have been overwritten correctly");
}
})
})
Loading
Loading