#37661 [SC-High] Incorrect `total_active_stake` reduction causes loss of funds for the users and excessive fees collection over time
Submitted on Dec 11th 2024 at 22:09:38 UTC by @holydevoti0n for Audit Comp | Folks: Liquid Staking
Report ID: #37661
Report Type: Smart Contract
Report severity: High
Target: https://github.com/Folks-Finance/algo-liquid-staking-contracts/blob/8bd890fde7981335e9b042a99db432e327681e1a/contracts/xalgo/consensus_v2.py
Impacts:
Permanent freezing of funds
Permanent freezing of unclaimed yield
Description
Brief/Intro
The burn operation incorrectly reduces total_active_stake
by including rewards in the reduction amount. This leads to inflated reward calculations, causing the protocol to collect excessive fees from user funds.
Vulnerability Details
When users burn xALGO, the contract incorrectly reduces total_active_stake
by the full withdrawal amount (stake + rewards
): https://github.com/Folks-Finance/algo-liquid-staking-contracts/blob/8bd890fde7981335e9b042a99db432e327681e1a/contracts/xalgo/consensus_v2.py#L824
def burn(send_xalgo: abi.AssetTransferTransaction, min_received: abi.Uint64) -> Expr:
burn_amount = send_xalgo.get().asset_amount()
algo_balance = ScratchVar(TealType.uint64)
algo_to_send = ScratchVar(TealType.uint64)
return Seq(
# calculate algo amount to send
@> algo_balance.store(
App.globalGet(total_active_stake_key)
@> + App.globalGet(total_rewards_key)
- App.globalGet(total_unclaimed_fees_key)
),
@> algo_to_send.store(
mul_scale(
burn_amount,
algo_balance.load(),
get_x_algo_circulating_supply() + burn_amount
)
),
...
# update total active stake
@> App.globalPut(total_active_stake_key, App.globalGet(total_active_stake_key) - algo_to_send.load()),
...
)
First burn distorts
total_active_stake
by includingrewards
in reduction.Subsequent burns use this distorted
total_active_stake
value.Creates compounding error where each burn further distorts the rate.
The second problem is that the total_active_stake
is used to calculate the protocol fees and the total in rewards: https://github.com/Folks-Finance/algo-liquid-staking-contracts/blob/8bd890fde7981335e9b042a99db432e327681e1a/contracts/xalgo/consensus_v2.py#L144-L147
def update_total_rewards_and_unclaimed_fees():
old_total_rewards = ScratchVar(TealType.uint64)
new_total_rewards = ScratchVar(TealType.uint64)
return Seq(
# update total rewards
old_total_rewards.store(App.globalGet(total_rewards_key)),
new_total_rewards.store(get_proposers_algo_balance(Int(0)) - App.globalGet(total_pending_stake_key) - App.globalGet(total_active_stake_key)),
@> App.globalPut(total_rewards_key, new_total_rewards.load()),
# update unclaimed fees
@> App.globalPut(total_unclaimed_fees_key, App.globalGet(total_unclaimed_fees_key) + mul_scale(
new_total_rewards.load() - old_total_rewards.load(),
App.globalGet(fee_key),
ONE_4_DP
)),
)
The artificially low total_active_stake
causes:
Inflated new_total_rewards calculation
Excessive fee collection since fees are taken as a percentage of rewards
Compounding effect as more burns occur over time
Impact Details
Loss of user funds through excessive fee collection(especially for the users that will keep the funds after several
burn
operations)Rewards and fees are calculated on stake that is incorrectly classified as rewards
Impact compounds over time with each burn operation
Recommendation
The burn operation should reduce total_active_stake
proportionally based only on the staked amount. i.e:
stake_reduction = mul_scale(
burn_amount,
App.globalGet(total_active_stake_key),
get_x_algo_circulating_supply()
)
App.globalPut(total_active_stake_key,
App.globalGet(total_active_stake_key) - stake_reduction)
Proof of Concept
The PoC shows the tracking of how total_active_stake
decreases more than it should, which causes the protocol to misclassify staked funds as rewards.
Another observation is that the helper functions of the PoC is merely copy/paste code from the original test suite. I've done this so I could separate the test in a new file but still preserve the state and use the same logic.
Create a new test file called incorrect-burning.test.ts
and paste the following code into it:
import {
ABIContract,
Account,
Algodv2,
AtomicTransactionComposer,
decodeAddress,
decodeUint64,
encodeUint64,
generateAccount,
getApplicationAddress,
getMethodByName,
IntDecoding,
makeApplicationCreateTxn,
makeApplicationUpdateTxn,
makeBasicAccountTransactionSigner,
modelsv2,
OnApplicationComplete,
} from "algosdk";
import { mulScale, mulScaleRoundUp, ONE_16_DP, ONE_4_DP } from "folks-finance-js-sdk";
import { sha256 } from "js-sha256";
import { prepareOptIntoAssetTxn } from "./transactions/common";
import {
parseXAlgoConsensusV1GlobalState,
prepareAddProposerForXAlgoConsensus,
prepareBurnFromXAlgoConsensusV2,
prepareClaimDelayedMintFromXAlgoConsensus,
prepareClaimXAlgoConsensusV2Fee,
prepareDelayedMintFromXAlgoConsensusV2,
prepareImmediateMintFromXAlgoConsensusV2,
prepareInitialiseXAlgoConsensusV1,
prepareInitialiseXAlgoConsensusV2,
preparePauseXAlgoConsensusMinting,
prepareRegisterXAlgoConsensusOffline,
prepareRegisterXAlgoConsensusOnline,
prepareScheduleXAlgoConsensusSCUpdate,
prepareUpdateXAlgoConsensusAdmin,
prepareUpdateXAlgoConsensusV2Fee,
prepareUpdateXAlgoConsensusPremium,
prepareUpdateXAlgoConsensusMaxProposerBalance,
prepareUpdateXAlgoConsensusSC,
prepareSetXAlgoConsensusProposerAdmin,
prepareSubscribeXAlgoConsensusProposerToXGov,
prepareUnsubscribeXAlgoConsensusProposerFromXGov,
prepareXAlgoConsensusDummyCall,
parseXAlgoConsensusV2GlobalState,
prepareCreateXAlgoConsensusV1,
prepareMintFromXAlgoConsensusV1,
} from "./transactions/xAlgoConsensus";
import { getABIContract } from "./utils/abi";
import { getAlgoBalance, getAssetBalance } from "./utils/account";
import {
compilePyTeal,
compileTeal,
enc,
getAppGlobalState,
getParsedValueFromState,
parseUint64s
} from "./utils/contracts";
import { fundAccountWithAlgo } from "./utils/fund";
import { privateAlgodClient, startPrivateNetwork, stopPrivateNetwork } from "./utils/privateNetwork";
import { advanceBlockRounds, advancePrevBlockTimestamp } from "./utils/time";
import { getParams, submitGroupTransaction, submitTransaction } from "./utils/transaction";
import fs from 'fs';
jest.setTimeout(1000000);
describe("Algo Consensus V2", () => {
let algodClient: Algodv2;
let prevBlockTimestamp: bigint;
let user1: Account = generateAccount();
let user2: Account = generateAccount();
let proposer0: Account = generateAccount();
let proposer1: Account = generateAccount();
let admin: Account = generateAccount();
let registerAdmin: Account = generateAccount();
let xGovAdmin: Account = generateAccount();
let proposerAdmin: Account = generateAccount();
let xAlgoAppId: number, xAlgoId: number;
let xAlgoConsensusABI: ABIContract;
let xGovRegistryAppId: number;
let xGovRegistryABI: ABIContract;
let xGovFee: bigint;
const timeDelay = BigInt(86400);
const minProposerBalance = BigInt(5e6);
const maxProposerBalance = BigInt(500e6);
const premium = BigInt(0.001e16); // 0.1%
const fee = BigInt(0.1e4); // 10%
const nonce = Uint8Array.from([0, 0]);
const resizeProposerBoxCost = BigInt(16000);
const updateSCBoxCost = BigInt(32100);
const delayMintBoxCost = BigInt(36100);
beforeAll(async () => {
console.log("Starting private network...");
await startPrivateNetwork();
const netConfig = Buffer.from(fs.readFileSync("net1/Primary/algod.net")).toString();
const [host, port] = netConfig.split(":");
console.log(`Using host: ${host}, port: ${port}`);
try {
const response = await fetch(`http://${host}:${port}/health`);
if (!response.ok) {
throw new Error(`Algod health check failed with status: ${response.status}`);
}
console.log("Algod is healthy.");
} catch (error) {
console.error("Failed to connect to Algod:", error);
throw error;
}
algodClient = privateAlgodClient();
algodClient.setIntEncoding(IntDecoding.MIXED);
// initialise accounts with algo
await fundAccountWithAlgo(algodClient, user1.addr, 1000e6, await getParams(algodClient));
await fundAccountWithAlgo(algodClient, user2.addr, 1000e6, await getParams(algodClient));
await fundAccountWithAlgo(algodClient, admin.addr, 1000e6, await getParams(algodClient));
await fundAccountWithAlgo(algodClient, registerAdmin.addr, 1000e6, await getParams(algodClient));
await fundAccountWithAlgo(algodClient, xGovAdmin.addr, 1000e6, await getParams(algodClient));
await fundAccountWithAlgo(algodClient, proposerAdmin.addr, 1000e6, await getParams(algodClient));
// advance time well past current time so we are dealing with deterministic time using offsets
prevBlockTimestamp = await advancePrevBlockTimestamp(algodClient, 1000);
// deploy xgov registry
const approval = await compileTeal(compilePyTeal('contracts/testing/xgov_registry'));
const clear = await compileTeal(compilePyTeal('contracts/common/clear_program', 10));
const tx = makeApplicationCreateTxn(user1.addr, await getParams(algodClient), OnApplicationComplete.NoOpOC, approval, clear, 0, 0, 1, 0);
const txId = await submitTransaction(algodClient, tx, user1.sk);
const txInfo = await algodClient.pendingTransactionInformation(txId).do();
xGovRegistryAppId = txInfo["application-index"];
xGovRegistryABI = getABIContract('contracts/testing/xgov_registry');
xGovFee = BigInt(getParsedValueFromState(await getAppGlobalState(algodClient, xGovRegistryAppId), "xgov_fee") || 0);
});
afterAll(() => {
stopPrivateNetwork();
});
describe("Burn", () => {
beforeEach(async () => {
await initializeXAlgoConsensusV2();
await updateAdmins();
await addProposer();
await setProposerAdmin();
await registerOnline();
await registerOffline();
await setupImmediateMint();
});
test("When burning, the protocol misclassify staked funds as rewards ", async () => {
// Initial setup - Send rewards to proposers to simulate earnings
const rewards = BigInt(10e6);
await fundAccountWithAlgo(algodClient, proposer0.addr, rewards);
await fundAccountWithAlgo(algodClient, proposer1.addr, rewards);
const proposerAddrs = [proposer0.addr, proposer1.addr];
// First mint
const mintAmount = BigInt(200e6);
const minReceived = BigInt(0);
let txns = [
prepareXAlgoConsensusDummyCall(xAlgoConsensusABI, xAlgoAppId, user1.addr, [], await getParams(algodClient)),
...prepareImmediateMintFromXAlgoConsensusV2(
xAlgoConsensusABI,
xAlgoAppId,
xAlgoId,
user1.addr,
mintAmount,
minReceived,
proposerAddrs,
await getParams(algodClient)
)
];
await submitGroupTransaction(algodClient, txns, txns.map(() => user1.sk));
// Get state after mint
let stateBefore = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
let { algoBalance: balanceBefore, xAlgoCirculatingSupply: supplyBefore } = await getXAlgoRate();
console.log("\nAfter Initial Mint:");
console.log("Total Active Stake:", stateBefore.totalActiveStake.toString());
console.log("Total Rewards:", stateBefore.totalRewards.toString());
console.log("ALGO Balance:", balanceBefore.toString());
console.log("xALGO Supply:", supplyBefore.toString());
// First burn - 25% of xALGO
const burnAmount1 = supplyBefore / BigInt(4);
txns = [
prepareXAlgoConsensusDummyCall(xAlgoConsensusABI, xAlgoAppId, user1.addr, [], await getParams(algodClient)),
...prepareBurnFromXAlgoConsensusV2(
xAlgoConsensusABI,
xAlgoAppId,
xAlgoId,
user1.addr,
burnAmount1,
BigInt(0),
proposerAddrs,
await getParams(algodClient)
)
];
await submitGroupTransaction(algodClient, txns, txns.map(() => user1.sk));
// Check state after first burn
let stateAfterBurn1 = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
let { algoBalance: balanceAfterBurn1 } = await getXAlgoRate();
console.log("\nAfter First Burn (25%):");
console.log("Total Active Stake:", stateAfterBurn1.totalActiveStake.toString());
console.log("Total Rewards:", stateAfterBurn1.totalRewards.toString());
console.log("ALGO Balance:", balanceAfterBurn1.toString());
// Calculate expected stake reduction for first burn
const expectedStakeReduction1 = (stateBefore.totalActiveStake * burnAmount1) / supplyBefore;
const actualStakeReduction1 = stateBefore.totalActiveStake - stateAfterBurn1.totalActiveStake;
console.log("\nFirst Burn Analysis:");
console.log("Expected Stake Reduction:", expectedStakeReduction1.toString());
console.log("Actual Stake Reduction:", actualStakeReduction1.toString());
// Verify first burn reduced stake too much
expect(actualStakeReduction1).toBeGreaterThan(expectedStakeReduction1);
// Second burn to demonstrate compounding error
const burnAmount2 = supplyBefore / BigInt(4);
txns = [
prepareXAlgoConsensusDummyCall(xAlgoConsensusABI, xAlgoAppId, user1.addr, [], await getParams(algodClient)),
...prepareBurnFromXAlgoConsensusV2(
xAlgoConsensusABI,
xAlgoAppId,
xAlgoId,
user1.addr,
burnAmount2,
BigInt(0),
proposerAddrs,
await getParams(algodClient)
)
];
await submitGroupTransaction(algodClient, txns, txns.map(() => user1.sk));
// Final state check
const finalState = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
// Calculate total expected stake reduction
const totalExpectedReduction = (expectedStakeReduction1 * BigInt(2));
const totalActualReduction = stateBefore.totalActiveStake - finalState.totalActiveStake;
console.log("\nFinal Analysis:");
console.log("Total Expected Reduction:", totalExpectedReduction.toString());
console.log("Total Actual Reduction:", totalActualReduction.toString());
console.log("Final Active Stake:", finalState.totalActiveStake.toString());
console.log("Final Rewards:", finalState.totalRewards.toString());
// Verify compounding error made total reduction too large
expect(totalActualReduction).toBeGreaterThan(totalExpectedReduction);
});
});
// HELPER FUNCTIONS
async function getXAlgoRate() {
const atc = new AtomicTransactionComposer();
atc.addMethodCall({
sender: user1.addr,
signer: makeBasicAccountTransactionSigner(user1),
appID: xAlgoAppId,
method: getMethodByName(xAlgoConsensusABI.methods, "get_xalgo_rate"),
methodArgs: [],
suggestedParams: await getParams(algodClient),
});
const simReq = new modelsv2.SimulateRequest({
txnGroups: [],
allowUnnamedResources: true,
})
const { methodResults } = await atc.simulate(algodClient, simReq);
const { returnValue } = methodResults[0];
const [algoBalance, xAlgoCirculatingSupply, balances]: [bigint, bigint, Uint8Array] = returnValue as any;
const proposersBalances = parseUint64s(Buffer.from(balances).toString("base64"));
return { algoBalance, xAlgoCirculatingSupply, proposersBalances } ;
}
// async function to initialize the application.
async function initializeXAlgoConsensusV2() {
// deploy algo consensus v1
const { tx: createTx, abi } = await prepareCreateXAlgoConsensusV1(admin.addr, admin.addr, registerAdmin.addr, minProposerBalance, maxProposerBalance, premium, fee, await getParams(algodClient));
xAlgoConsensusABI = abi;
let txId = await submitTransaction(algodClient, createTx, admin.sk);
let txInfo = await algodClient.pendingTransactionInformation(txId).do();
xAlgoAppId = txInfo["application-index"];
// fund minimum balance
await fundAccountWithAlgo(algodClient, proposer0.addr, BigInt(0.1e6), await getParams(algodClient));
await fundAccountWithAlgo(algodClient, getApplicationAddress(xAlgoAppId), 0.6034e6);
// initialise algo consensus v1
const initTxns = prepareInitialiseXAlgoConsensusV1(xAlgoConsensusABI, xAlgoAppId, admin.addr, proposer0.addr, await getParams(algodClient));
[, txId] = await submitGroupTransaction(algodClient, initTxns, [proposer0.sk, admin.sk]);
txInfo = await algodClient.pendingTransactionInformation(txId).do();
xAlgoId = txInfo['inner-txns'][0]['asset-index'];
// verify xAlgo was created
const assetInfo = await algodClient.getAssetByID(xAlgoId).do();
expect(assetInfo.params.creator).toEqual(getApplicationAddress(xAlgoAppId));
expect(assetInfo.params.reserve).toEqual(getApplicationAddress(xAlgoAppId));
expect(assetInfo.params.total).toEqual(BigInt(10e15));
expect(assetInfo.params.decimals).toEqual(6);
expect(assetInfo.params.name).toEqual('Governance xAlgo');
expect(assetInfo.params['unit-name']).toEqual('xALGO');
// opt into xALGO
let optInTx = prepareOptIntoAssetTxn(admin.addr, xAlgoId, await getParams(algodClient));
await submitTransaction(algodClient, optInTx, admin.sk);
optInTx = prepareOptIntoAssetTxn(user1.addr, xAlgoId, await getParams(algodClient));
await submitTransaction(algodClient, optInTx, user1.sk);
optInTx = prepareOptIntoAssetTxn(user2.addr, xAlgoId, await getParams(algodClient));
await submitTransaction(algodClient, optInTx, user2.sk);
// mint to get pool started
const mintAmount = BigInt(100e6);
const mintTxns = prepareMintFromXAlgoConsensusV1(xAlgoConsensusABI, xAlgoAppId, xAlgoId, user2.addr, mintAmount, proposer0.addr, await getParams(algodClient));
await submitGroupTransaction(algodClient, mintTxns, mintTxns.map(() => user2.sk));
// verify global state
const state = await parseXAlgoConsensusV1GlobalState(algodClient, xAlgoAppId);
expect(state.initialised).toEqual(true);
expect(state.admin).toEqual(admin.addr);
expect(state.registerAdmin).toEqual(registerAdmin.addr);
expect(state.xAlgoId).toEqual(xAlgoId);
expect(state.numProposers).toEqual(BigInt(1));
expect(state.minProposerBalance).toEqual(minProposerBalance);
expect(state.maxProposerBalance).toEqual(maxProposerBalance);
expect(state.fee).toEqual(fee);
expect(state.premium).toEqual(premium);
expect(state.totalPendingStake).toEqual(BigInt(0));
expect(state.totalActiveStake).toEqual(mintAmount);
expect(state.totalRewards).toEqual(BigInt(0));
expect(state.totalUnclaimedFees).toEqual(BigInt(0));
expect(state.canImmediateMint).toEqual(true);
expect(state.canDelayMint).toEqual(false);
// verify proposers box
const proposersBox = await algodClient.getApplicationBoxByName(xAlgoAppId, enc.encode("pr")).do();
const proposers = new Uint8Array(960);
proposers.set(decodeAddress(proposer0.addr).publicKey, 0);
expect(proposersBox.value).toEqual(proposers);
// verify added proposer box
const boxName = Uint8Array.from([...enc.encode("ap"), ...decodeAddress(proposer0.addr).publicKey]);
const addedProposerBox = await algodClient.getApplicationBoxByName(xAlgoAppId, boxName).do();
expect(addedProposerBox.value).toEqual(new Uint8Array(0));
// verify balances
const user2XAlgoBalance = await getAssetBalance(algodClient, user2.addr, xAlgoId);
expect(user2XAlgoBalance).toEqual(mintAmount);
// update to algo consensus v2
const approval = await compileTeal(compilePyTeal('contracts/xalgo/consensus_v2'));
const clear = await compileTeal(compilePyTeal('contracts/common/clear_program', 10));
const updateTx = makeApplicationUpdateTxn(admin.addr, await getParams(algodClient), xAlgoAppId, approval, clear);
await submitTransaction(algodClient, updateTx, admin.sk);
xAlgoConsensusABI = getABIContract('contracts/xalgo/consensus_v2');
// initialise
const oldState = await parseXAlgoConsensusV1GlobalState(algodClient, xAlgoAppId);
// initialise
const tx = prepareInitialiseXAlgoConsensusV2(xAlgoConsensusABI, xAlgoAppId, admin.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
// verify global state
const unformattedState = await getAppGlobalState(algodClient, xAlgoAppId);
expect(getParsedValueFromState(unformattedState, "initialised")).toBeUndefined();
expect(getParsedValueFromState(unformattedState, "min_proposer_balance")).toBeUndefined();
const newState = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(newState.initialised).toEqual(true);
expect(newState.admin).toEqual(oldState.admin);
expect(newState.xGovAdmin).toEqual(oldState.admin);
expect(newState.registerAdmin).toEqual(oldState.registerAdmin);
expect(newState.xAlgoId).toEqual(oldState.xAlgoId);
expect(newState.numProposers).toEqual(oldState.numProposers);
expect(newState.maxProposerBalance).toEqual(oldState.maxProposerBalance);
expect(newState.fee).toEqual(oldState.fee);
expect(newState.premium).toEqual(oldState.premium);
expect(newState.totalPendingStake).toEqual(oldState.totalPendingStake);
expect(newState.totalActiveStake).toEqual(oldState.totalActiveStake);
expect(newState.totalRewards).toEqual(oldState.totalRewards);
expect(newState.totalUnclaimedFees).toEqual(oldState.totalUnclaimedFees);
expect(newState.canImmediateMint).toEqual(oldState.canImmediateMint);
expect(newState.canDelayMint).toEqual(oldState.canDelayMint);
}
async function setupImmediateMint() {
const { canImmediateMint, canDelayMint } = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
// update pause minting
let tx = preparePauseXAlgoConsensusMinting(xAlgoConsensusABI, xAlgoAppId, admin.addr, "can_immediate_mint", canImmediateMint, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
tx = preparePauseXAlgoConsensusMinting(xAlgoConsensusABI, xAlgoAppId, admin.addr, "can_delay_mint", canDelayMint, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
let state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.canImmediateMint).toEqual(!canImmediateMint);
expect(state.canDelayMint).toEqual(!canDelayMint);
// restore pause minting
tx = preparePauseXAlgoConsensusMinting(xAlgoConsensusABI, xAlgoAppId, admin.addr, "can_immediate_mint", !canImmediateMint, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
tx = preparePauseXAlgoConsensusMinting(xAlgoConsensusABI, xAlgoAppId, admin.addr, "can_delay_mint", !canDelayMint, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.canImmediateMint).toEqual(canImmediateMint);
expect(state.canDelayMint).toEqual(canDelayMint);
}
async function addProposer() {
const minBalance = BigInt(16100);
await fundAccountWithAlgo(algodClient, getApplicationAddress(xAlgoAppId), minBalance, await getParams(algodClient));
await fundAccountWithAlgo(algodClient, proposer1.addr, BigInt(0.1e6), await getParams(algodClient));
// balances before
const proposerAlgoBalanceB = await getAlgoBalance(algodClient, proposer0.addr);
const appAlgoBalanceB = await getAlgoBalance(algodClient, getApplicationAddress(xAlgoAppId));
let state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
const { totalActiveStake: oldTotalActiveStake } = state;
// register
const txns = prepareAddProposerForXAlgoConsensus(xAlgoConsensusABI, xAlgoAppId, registerAdmin.addr, proposer1.addr, await getParams(algodClient));
const [, txId] = await submitGroupTransaction(algodClient, txns, [proposer1.sk, registerAdmin.sk]);
const txInfo = await algodClient.pendingTransactionInformation(txId).do();
state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.totalActiveStake).toEqual(oldTotalActiveStake);
// balances after
const { proposersBalances } = await getXAlgoRate();
const proposerAlgoBalanceA = await getAlgoBalance(algodClient, proposer0.addr);
const appAlgoBalanceA = await getAlgoBalance(algodClient, getApplicationAddress(xAlgoAppId));
expect(proposersBalances.length).toEqual(2);
expect(proposersBalances[1]).toEqual(BigInt(0.1e6));
expect(proposerAlgoBalanceA).toEqual(proposerAlgoBalanceB);
expect(appAlgoBalanceA).toEqual(appAlgoBalanceB);
expect(txInfo['inner-txns']).toBeUndefined();
// verify proposers box
const proposersBox = await algodClient.getApplicationBoxByName(xAlgoAppId, enc.encode("pr")).do();
const proposers = new Uint8Array(960);
proposers.set(decodeAddress(proposer0.addr).publicKey, 0);
proposers.set(decodeAddress(proposer1.addr).publicKey, 32);
expect(proposersBox.value).toEqual(proposers);
// verify added proposer box
const boxName = Uint8Array.from([...enc.encode("ap"), ...decodeAddress(proposer1.addr).publicKey]);
const addedProposerBox = await algodClient.getApplicationBoxByName(xAlgoAppId, boxName).do();
expect(addedProposerBox.value).toEqual(new Uint8Array(0));
}
async function updateAdmins() {
// admin updating admin
let tx = prepareUpdateXAlgoConsensusAdmin(xAlgoConsensusABI, xAlgoAppId, "admin", admin.addr, user1.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
let state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.admin).toEqual(user1.addr);
// restore old admin
tx = prepareUpdateXAlgoConsensusAdmin(xAlgoConsensusABI, xAlgoAppId, "admin", user1.addr, admin.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, user1.sk);
state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.admin).toEqual(admin.addr);
// admin updating register admin
tx = prepareUpdateXAlgoConsensusAdmin(xAlgoConsensusABI, xAlgoAppId, "register_admin", admin.addr, user1.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.registerAdmin).toEqual(user1.addr);
// register admin updating register admin
tx = prepareUpdateXAlgoConsensusAdmin(xAlgoConsensusABI, xAlgoAppId, "register_admin", user1.addr, registerAdmin.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, user1.sk);
state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.registerAdmin).toEqual(registerAdmin.addr);
// admin updating xgov admin
tx = prepareUpdateXAlgoConsensusAdmin(xAlgoConsensusABI, xAlgoAppId, "xgov_admin", admin.addr, user1.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.xGovAdmin).toEqual(user1.addr);
// xgov admin updating xgov admin
tx = prepareUpdateXAlgoConsensusAdmin(xAlgoConsensusABI, xAlgoAppId, "xgov_admin", user1.addr, xGovAdmin.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, user1.sk);
state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.xGovAdmin).toEqual(xGovAdmin.addr);
}
async function updatePremium() {
// update premium
const tempPremium = BigInt(0.0025e16);
let tx = prepareUpdateXAlgoConsensusPremium(xAlgoConsensusABI, xAlgoAppId, admin.addr, tempPremium, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
let state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.premium).toEqual(tempPremium);
// restore old premium
tx = prepareUpdateXAlgoConsensusPremium(xAlgoConsensusABI, xAlgoAppId, admin.addr, premium, await getParams(algodClient));
await submitTransaction(algodClient, tx, admin.sk);
state = await parseXAlgoConsensusV2GlobalState(algodClient, xAlgoAppId);
expect(state.premium).toEqual(premium);
}
async function setProposerAdmin() {
const boxName = Uint8Array.from([...enc.encode("ap"), ...decodeAddress(proposer0.addr).publicKey]);
let addedProposerBox = await algodClient.getApplicationBoxByName(xAlgoAppId, boxName).do();
expect(addedProposerBox.value).toEqual(new Uint8Array(0));
// fund box
await fundAccountWithAlgo(algodClient, getApplicationAddress(xAlgoAppId), resizeProposerBoxCost, await getParams(algodClient));
// immediate if no existing proposer admin
let tx = prepareSetXAlgoConsensusProposerAdmin(xAlgoConsensusABI, xAlgoAppId, registerAdmin.addr, 0, proposer0.addr, user1.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, registerAdmin.sk)
addedProposerBox = await algodClient.getApplicationBoxByName(xAlgoAppId, boxName).do();
expect(addedProposerBox.value).toEqual(Uint8Array.from([...encodeUint64(prevBlockTimestamp), ...decodeAddress(user1.addr).publicKey]));
// delay if existing proposer admin
tx = prepareSetXAlgoConsensusProposerAdmin(xAlgoConsensusABI, xAlgoAppId, registerAdmin.addr, 0, proposer0.addr, proposerAdmin.addr, await getParams(algodClient));
await submitTransaction(algodClient, tx, registerAdmin.sk)
addedProposerBox = await algodClient.getApplicationBoxByName(xAlgoAppId, boxName).do();
expect(addedProposerBox.value).toEqual(Uint8Array.from([...encodeUint64(prevBlockTimestamp + timeDelay), ...decodeAddress(proposerAdmin.addr).publicKey]));
// proceed to timestamp after timeDelay
const box = await algodClient.getApplicationBoxByName(xAlgoAppId, boxName).do();
const ts = decodeUint64(box.value.subarray(0, 8), "bigint");
const offset = Number(ts - prevBlockTimestamp) + 1;
prevBlockTimestamp = await advancePrevBlockTimestamp(algodClient, offset);
}
async function registerOnline() {
const registerFeeAmount = BigInt(2e6);
const voteKey = Buffer.from("G/lqTV6MKspW6J8wH2d8ZliZ5XZVZsruqSBJMwLwlmo=", "base64");
const selKey = Buffer.from("LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=", "base64");
const stateProofKey = Buffer.from("Nn0fiJDZH2wyLqxNzrOC3WPF8Vz3AH8JU1IGI2H2xdcnRiqw7YuWkohuKHpC1EJMAe6ZVbUS/S2rPeCRAolfRQ==", "base64");
const voteFirstRound = 1;
const voteLastRound = 5000;
const voteKeyDilution = 1500;
const txns = prepareRegisterXAlgoConsensusOnline(xAlgoConsensusABI, xAlgoAppId, proposerAdmin.addr, registerFeeAmount, 0, proposer0.addr, voteKey, selKey, stateProofKey, voteFirstRound, voteLastRound, voteKeyDilution, await getParams(algodClient));
const [, txId] = await submitGroupTransaction(algodClient, txns, txns.map(() => proposerAdmin.sk));
const txInfo = await algodClient.pendingTransactionInformation(txId).do();
// check key registration
const innerRegisterOnlineTx = txInfo['inner-txns'][0]['txn']['txn'];
expect(innerRegisterOnlineTx.type).toEqual('keyreg');
expect(innerRegisterOnlineTx.snd).toEqual(Uint8Array.from(decodeAddress(proposer0.addr).publicKey));
expect(innerRegisterOnlineTx.votekey).toEqual(Uint8Array.from(voteKey));
expect(innerRegisterOnlineTx.selkey).toEqual(Uint8Array.from(selKey));
expect(innerRegisterOnlineTx.sprfkey).toEqual(Uint8Array.from(stateProofKey));
expect(innerRegisterOnlineTx.votefst).toEqual(voteFirstRound);
expect(innerRegisterOnlineTx.votelst).toEqual(voteLastRound);
expect(innerRegisterOnlineTx.votekd).toEqual(voteKeyDilution);
expect(innerRegisterOnlineTx.fee).toEqual(Number(registerFeeAmount));
}
async function registerOffline() {
for (const sender of [registerAdmin, proposerAdmin]) {
const tx = prepareRegisterXAlgoConsensusOffline(xAlgoConsensusABI, xAlgoAppId, sender.addr, 0, proposer0.addr, await getParams(algodClient));
const txId = await submitTransaction(algodClient, tx, sender.sk);
const txInfo = await algodClient.pendingTransactionInformation(txId).do();
// check key registration
const innerRegisterOnlineTx = txInfo['inner-txns'][0]['txn']['txn'];
expect(innerRegisterOnlineTx.type).toEqual('keyreg');
expect(innerRegisterOnlineTx.snd).toEqual(Uint8Array.from(decodeAddress(proposer0.addr).publicKey));
expect(innerRegisterOnlineTx.votekey).toBeUndefined();
expect(innerRegisterOnlineTx.selkey).toBeUndefined();
expect(innerRegisterOnlineTx.sprfkey).toBeUndefined();
expect(innerRegisterOnlineTx.votefst).toBeUndefined();
expect(innerRegisterOnlineTx.votelst).toBeUndefined();
expect(innerRegisterOnlineTx.votekd).toBeUndefined();
expect(innerRegisterOnlineTx.fee).toBeUndefined();
}
}
});
then run: PYTHONPATH="./contracts" npx jest test/incorrect-burning.test.ts --runInBand
Output:
Final Analysis:
Total Expected Reduction: 149999998
Total Actual Reduction: 158999998
Final Active Stake: 141000002
Final Rewards: 20000000
PASS test/incorrect-burning.test.ts (11.539 s)
Algo Consensus V2 Burn
✓ When burning, the protocol misclassify staked funds as rewards (4263 ms)