Skip to content
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
1b5dbd8
feat: add queued stake updates and withdrawals
0xCardiE Apr 13, 2026
8aa960c
decouple main function into separate methods
0xCardiE Apr 13, 2026
1d4543c
fix: block queued withdrawals during freezes and active rounds
0xCardiE Apr 13, 2026
e2f2542
fix: make redistribution payout and linkage atomic
0xCardiE Apr 13, 2026
9f9d1cc
test: harden staking and redistribution coverage
0xCardiE Apr 13, 2026
035ebc8
reconcile slashing
0xCardiE Apr 13, 2026
bf76cb6
fix: align queued stake previews with round-based checks
0xCardiE Apr 13, 2026
10197f9
add comments for staking functions
0xCardiE Apr 13, 2026
f5c74b8
fix: close exit queues and extend withdrawal delay
0xCardiE Apr 13, 2026
01bc6ff
formatting
0xCardiE Apr 14, 2026
e6d7848
Merge remote-tracking branch 'origin/master' into feat/new_staking
0xCardiE Apr 14, 2026
d88be86
refactor: simplify stake initialization state
0xCardiE Apr 15, 2026
9b5d271
fix: execute effective stake withdrawals without redistribution relin…
0xCardiE Apr 15, 2026
ba86d95
loosen checks per SWIP format
0xCardiE Apr 16, 2026
c73b978
refactor: rename queued stake previews to lookahead
0xCardiE Apr 17, 2026
dff2544
fix(staking): harden error paths and visibility
0xCardiE Apr 18, 2026
8bba0ee
refactor(staking): remove dead code and fix privileged caller reverts
0xCardiE Apr 18, 2026
3ece9d1
ci: trigger CI run
0xCardiE Apr 18, 2026
73816e9
style: format Staking.sol with prettier
0xCardiE Apr 18, 2026
36e003e
refactor(staking): remove _revertOnFrozen parameter
0xCardiE Apr 20, 2026
7840796
fix(staking): apply ready updates before freezing
0xCardiE Apr 21, 2026
c0c012c
feat(staking): return effectiveFromRound from enqueue calls
0xCardiE May 6, 2026
285659d
fix(staking): applyUpdates freeze guard and withdrawal fixes
0xCardiE May 6, 2026
27aa102
docs(staking): NatSpec errors and diagnostic revert args
0xCardiE May 6, 2026
e989f4c
refactor(staking): review items—networkId, reconcile, coverage tests
0xCardiE May 6, 2026
e11a04f
test(staking): cover enqueue returns, no-ops, queue limits
0xCardiE May 6, 2026
89d93d5
chore(lint): format Staking.sol and clean tests
0xCardiE May 6, 2026
97c0d7e
refactor(staking): fold full withdraw into BelowMinimumStake
0xCardiE May 7, 2026
9d0a054
test(staking): add FIFO queue and mixed-delay cases
0xCardiE May 7, 2026
9db52e2
test(staking): cover redeposit after exit completes
0xCardiE May 7, 2026
b3562c4
feat(staking): persist protocol freeze per account
0xCardiE May 18, 2026
56e4c04
remove change of network by admin - security
0xCardiE May 18, 2026
ce7e917
fix various staking issues
0xCardiE May 18, 2026
9d9fdb8
code fixes and optimizations
0xCardiE May 18, 2026
261e199
docs(staking): clarify FrozenWithdrawal and satisfy Prettier CI
0xCardiE May 18, 2026
e5ee89b
test(staking): add missing QueueClosed entry to errors map
0xCardiE May 19, 2026
706b292
Migrate freeze test
0xCardiE May 19, 2026
332070d
refactor(staking): move mutators to changing section
0xCardiE May 20, 2026
b984e2a
docs(staking): clarify queue break on frozen withdrawal
0xCardiE May 20, 2026
d8e8091
docs(staking): document _applyStoredUpdate paths
0xCardiE May 20, 2026
2d547a4
Add freeze migration function
0xCardiE May 20, 2026
eb8e635
refactor(staking): rename _applyPreviewUpdate to _simulateUpdate
0xCardiE May 20, 2026
7102938
refactor(staking): consolidate per-address Account
0xCardiE May 21, 2026
5086b5b
style(staking): format Staking.sol with Prettier
0xCardiE May 21, 2026
defe2d8
Merge remote-tracking branch 'origin/master' into feat/new_staking
0xCardiE May 22, 2026
9bebce5
Add Echidna harness for queued-update StakeRegistry
0xCardiE May 22, 2026
401bfd3
style(echidna): format staking harnesses with Prettier
0xCardiE May 22, 2026
1413ca2
refactor(staking): nest UpdateQueue in Account
0xCardiE May 22, 2026
ef2c940
fix description
0xCardiE Jun 1, 2026
68c29df
Introducing constants to use accross the contracts
0xCardiE Jun 1, 2026
62f9737
add other values to constants as well, so we have it all in one place…
0xCardiE Jun 1, 2026
f1a8338
remove network ID change
0xCardiE Jun 1, 2026
71db344
clean echidna and some refactoring
0xCardiE Jun 1, 2026
fc6b4c3
fixed mumbo jumbo :)
0xCardiE Jun 1, 2026
3c670ca
no reuse remove
0xCardiE Jun 1, 2026
04882d0
Add modifier which is reused
0xCardiE Jun 1, 2026
0cb6053
remove duplicate
0xCardiE Jun 1, 2026
c42c12f
remove naming incosistencies
0xCardiE Jun 1, 2026
7181cc9
Prefix function parameters in Redistribution and PriceOracle.
0xCardiE Jun 1, 2026
99241ad
update docs
0xCardiE Jun 2, 2026
7495f08
refactor(staking): remove slash penalty support
0xCardiE Jun 8, 2026
044f5dc
Add undefined check
0xCardiE Jun 9, 2026
4345fdb
test(staking): cover remaining revert paths and StakeFrozen
0xCardiE Jun 26, 2026
8e1097a
docs(staking): add test coverage section, sync with contract
0xCardiE Jun 26, 2026
f0d5ee8
change ignore file
0xCardiE Jun 30, 2026
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ This project includes the following smart contracts and their metadata:
- HitchensOrderStatisticsTreeLib
- Test Token

**`StakeRegistry` / `applyUpdates` (bots & backends):** `applyUpdates(address)` applies the ready prefix of each owner’s update queue. If the next pending item at the queue head is a **due** `WithdrawTokens` or `ExitStake` and payouts are blocked by an active freeze, the call **reverts** with `FrozenWithdrawal()` and the **whole transaction rolls back** (no partial commit from that call). Integrators should treat that as retry-after-unfreeze, or rely on paths that advance the queue internally under different constraints (redistributor-triggered txs, paused migration flows). Inline NatSpec next to `applyUpdates` matches this behaviour.

- Metadata ([Testnet](./testnet_deployed.json),[Mainnet](./mainnet_deployed.json))
- **Chain ID**: Chain ID of the blockchain.
- **Network ID**: Network ID.
Expand Down
12 changes: 9 additions & 3 deletions deploy/local/003_deploy_staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import { networkConfig } from '../../helper-hardhat-config';
const func: DeployFunction = async function ({ deployments, getNamedAccounts, network }) {
const { deploy, get, log } = deployments;
const { deployer } = await getNamedAccounts();
const swarmNetworkID = networkConfig[network.name]?.swarmNetworkId;
const config = networkConfig[network.name] || {};
const swarmNetworkID = config.swarmNetworkId;

const token = await get('TestToken');
const oracleAddress = (await get('PriceOracle')).address;

const args = [token.address, swarmNetworkID, oracleAddress];
const args = [
token.address,
swarmNetworkID,
config.stakeWaitBase || 2,
config.stakeWaitOverlayChange || 2,
config.stakeWaitWithdrawal || 2,
];
await deploy('StakeRegistry', {
from: deployer,
args: args,
Expand Down
12 changes: 9 additions & 3 deletions deploy/main/003_deploy_staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import { networkConfig } from '../../helper-hardhat-config';
const func: DeployFunction = async function ({ deployments, getNamedAccounts, network }) {
const { deploy, log, get } = deployments;
const { deployer } = await getNamedAccounts();
const swarmNetworkID = networkConfig[network.name]?.swarmNetworkId;
const config = networkConfig[network.name] || {};
const swarmNetworkID = config.swarmNetworkId;
const token = await get('Token');
const oracleAddress = (await get('PriceOracle')).address;

const args = [token.address, swarmNetworkID, oracleAddress];
const args = [
token.address,
swarmNetworkID,
config.stakeWaitBase || 2,
config.stakeWaitOverlayChange || 2,
config.stakeWaitWithdrawal || 2,
];
await deploy('StakeRegistry', {
from: deployer,
args: args,
Expand Down
14 changes: 11 additions & 3 deletions deploy/main/010_deploy_verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const func: DeployFunction = async function ({ deployments, network }) {
const { log, get } = deployments;

if (network.name == 'mainnet' && process.env.MAINNET_ETHERSCAN_KEY) {
const swarmNetworkID = networkConfig[network.name]?.swarmNetworkId;
const config = networkConfig[network.name] || {};
const swarmNetworkID = config.swarmNetworkId;
const token = await get('Token');

// Verify postageStamp
Expand All @@ -27,14 +28,21 @@ const func: DeployFunction = async function ({ deployments, network }) {

// Verify staking
const staking = await get('StakeRegistry');
const argStaking = [token.address, swarmNetworkID, priceOracle.address];
const redistribution = await get('Redistribution');
const argStaking = [
token.address,
redistribution.address,
swarmNetworkID,
Comment thread
0xCardiE marked this conversation as resolved.
config.stakeWaitBase || 2,
config.stakeWaitOverlayChange || 2,
config.stakeWaitWithdrawal || 2,
];

log('Verifying...');
await verify(staking.address, argStaking);
log('----------------------------------------------------');

// Verify redistribution
const redistribution = await get('Redistribution');
const argRedistribution = [staking.address, postageStamp.address, priceOracle.address];

log('Verifying...');
Expand Down
12 changes: 9 additions & 3 deletions deploy/test/003_deploy_staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import { networkConfig } from '../../helper-hardhat-config';
const func: DeployFunction = async function ({ deployments, getNamedAccounts, network }) {
const { deploy, log, get } = deployments;
const { deployer } = await getNamedAccounts();
const swarmNetworkID = networkConfig[network.name]?.swarmNetworkId;
const config = networkConfig[network.name] || {};
const swarmNetworkID = config.swarmNetworkId;
const token = await get('TestToken');
const oracleAddress = (await get('PriceOracle')).address;

const args = [token.address, swarmNetworkID, oracleAddress];
const args = [
token.address,
swarmNetworkID,
config.stakeWaitBase || 2,
config.stakeWaitOverlayChange || 2,
config.stakeWaitWithdrawal || 2,
];
await deploy('StakeRegistry', {
from: deployer,
args: args,
Expand Down
14 changes: 11 additions & 3 deletions deploy/test/010_deploy_verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const func: DeployFunction = async function ({ deployments, network }) {
const { log, get } = deployments;

if (process.env.TESTNET_ETHERSCAN_KEY) {
const swarmNetworkID = networkConfig[network.name]?.swarmNetworkId;
const config = networkConfig[network.name] || {};
const swarmNetworkID = config.swarmNetworkId;

// Verify TestNet token
const token = await get('TestToken');
Expand Down Expand Up @@ -34,14 +35,21 @@ const func: DeployFunction = async function ({ deployments, network }) {

// Verify staking
const staking = await get('StakeRegistry');
const argStaking = [token.address, swarmNetworkID, priceOracle.address];
const redistribution = await get('Redistribution');
const argStaking = [
token.address,
redistribution.address,
swarmNetworkID,
config.stakeWaitBase || 2,
config.stakeWaitOverlayChange || 2,
config.stakeWaitWithdrawal || 2,
];

log('Staking');
await verify(staking.address, argStaking);
log('----------------------------------------------------');

// Verify redistribution
const redistribution = await get('Redistribution');
const argRedistribution = [staking.address, postageStamp.address, priceOracle.address];

log('Redistribution');
Expand Down
42 changes: 39 additions & 3 deletions helper-hardhat-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,70 @@ export interface networkConfigItem {
blockConfirmations?: number;
swarmNetworkId?: number;
multisig?: string;
stakeWaitBase?: number;
stakeWaitOverlayChange?: number;
stakeWaitWithdrawal?: number;
}
export interface networkConfigInfo {
[key: string]: networkConfigItem;
}

const ROUNDS_PER_DAY = 114;
const WITHDRAWAL_WAIT_ROUNDS = ROUNDS_PER_DAY * 28;

export const networkConfig: networkConfigInfo = {
localhost: { swarmNetworkId: 0, multisig: '0x62cab2b3b55f341f10348720ca18063cdb779ad5' },
hardhat: { swarmNetworkId: 0, multisig: '0x62cab2b3b55f341f10348720ca18063cdb779ad5' },
localcluster: { swarmNetworkId: 0, multisig: '0x62cab2b3b55f341f10348720ca18063cdb779ad5' },
localhost: {
swarmNetworkId: 0,
multisig: '0x62cab2b3b55f341f10348720ca18063cdb779ad5',
stakeWaitBase: 2,
stakeWaitOverlayChange: 2,
stakeWaitWithdrawal: 2,
},
hardhat: {
swarmNetworkId: 0,
multisig: '0x62cab2b3b55f341f10348720ca18063cdb779ad5',
stakeWaitBase: 2,
stakeWaitOverlayChange: 2,
stakeWaitWithdrawal: 2,
},
localcluster: {
swarmNetworkId: 0,
multisig: '0x62cab2b3b55f341f10348720ca18063cdb779ad5',
stakeWaitBase: 2,
stakeWaitOverlayChange: 2,
stakeWaitWithdrawal: 2,
},
testnetlight: {
blockConfirmations: 6,
swarmNetworkId: 5,
multisig: '0xb1C7F17Ed88189Abf269Bf68A3B2Ed83C5276aAe',
stakeWaitBase: 2,
stakeWaitOverlayChange: 2,
stakeWaitWithdrawal: WITHDRAWAL_WAIT_ROUNDS,
},
testnet: {
blockConfirmations: 6,
swarmNetworkId: 10,
multisig: '0xb1C7F17Ed88189Abf269Bf68A3B2Ed83C5276aAe',
stakeWaitBase: 2,
stakeWaitOverlayChange: 2,
stakeWaitWithdrawal: WITHDRAWAL_WAIT_ROUNDS,
},
tenderly: {
blockConfirmations: 1,
swarmNetworkId: 1,
multisig: '0xb1C7F17Ed88189Abf269Bf68A3B2Ed83C5276aAe',
stakeWaitBase: 2,
stakeWaitOverlayChange: 2,
stakeWaitWithdrawal: WITHDRAWAL_WAIT_ROUNDS,
},
mainnet: {
blockConfirmations: 6,
swarmNetworkId: 1,
multisig: '0xD5C070FEb5EA883063c183eDFF10BA6836cf9816',
stakeWaitBase: 2,
stakeWaitOverlayChange: 2,
stakeWaitWithdrawal: WITHDRAWAL_WAIT_ROUNDS,
},
};

Expand Down
56 changes: 21 additions & 35 deletions src/Redistribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,19 @@ interface IPriceOracle {
}

interface IStakeRegistry {
struct Stake {
bytes32 overlay;
uint256 stakeAmount;
uint256 lastUpdatedBlockNumber;
}

function freezeDeposit(address _owner, uint256 _time) external;

function lastUpdatedBlockNumberOfAddress(address _owner) external view returns (uint256);

function overlayOfAddress(address _owner) external view returns (bytes32);

function overlayOfAddressLookahead(address _owner, uint64 _lookahead) external view returns (bytes32);

function heightOfAddress(address _owner) external view returns (uint8);

function heightOfAddressLookahead(address _owner, uint64 _lookahead) external view returns (uint8);

function nodeEffectiveStake(address _owner) external view returns (uint256);

function nodeEffectiveStakeLookahead(address _owner, uint64 _lookahead) external view returns (uint256);
}

/**
Expand Down Expand Up @@ -200,11 +198,6 @@ contract Redistribution is AccessControl, Pausable {
*/
event PriceAdjustmentSkipped(uint16 redundancyCount);

/**
* @dev Withdraw not successful in claim
*/
event WithdrawFailed(address owner);

/**
* @dev Logs that an overlay has revealed
*/
Expand All @@ -230,7 +223,6 @@ contract Redistribution is AccessControl, Pausable {
error CommitRoundOver(); // Commit phase in this round is over
error CommitRoundNotStarted(); // Commit phase in this round has not started yet
error NotMatchingOwner(); // Sender of commit is not matching the overlay address
error MustStake2Rounds(); // Before entering the game node must stake 2 rounds prior
error NotStaked(); // Node didn't add any staking
error WrongPhase(); // Checking in wrong phase, need to check duing claim phase of current round for next round or commit in current round
error AlreadyCommitted(); // Node already committed in this round
Expand Down Expand Up @@ -293,17 +285,12 @@ contract Redistribution is AccessControl, Pausable {
uint64 cr = currentRound();
bytes32 _overlay = Stakes.overlayOfAddress(msg.sender);
uint256 _stake = Stakes.nodeEffectiveStake(msg.sender);
uint256 _lastUpdate = Stakes.lastUpdatedBlockNumberOfAddress(msg.sender);
uint8 _height = Stakes.heightOfAddress(msg.sender);

if (_lastUpdate == 0) {
if (_stake == 0) {
revert NotStaked();
}

if (_lastUpdate >= block.number - 2 * ROUND_LENGTH) {
revert MustStake2Rounds();
}

if (cr > _roundNumber) {
revert CommitRoundOver();
}
Expand Down Expand Up @@ -482,13 +469,7 @@ contract Redistribution is AccessControl, Pausable {

estimateSize(entryProofLast.proofSegments[0]);

// Do the check if the withdraw was success
(bool success, ) = address(PostageContract).call(
abi.encodeWithSignature("withdraw(address)", winnerSelected.owner)
);
if (!success) {
emit WithdrawFailed(winnerSelected.owner);
}
PostageContract.withdraw(winnerSelected.owner);

emit WinnerSelected(winnerSelected);
emit ChunkCount(PostageContract.validChunkCount());
Expand Down Expand Up @@ -824,26 +805,31 @@ contract Redistribution is AccessControl, Pausable {

/**
* @notice Determine if a the owner of a given overlay can participate in the upcoming round.
* @dev This method is part of the external interface used by Bee nodes to pre-check
* eligibility, so it must remain available even when it is not referenced by on-chain code.
* @param _owner The address of the applicant from.
* @param _depth The storage depth the applicant intends to report.
*/
function isParticipatingInUpcomingRound(address _owner, uint8 _depth) public view returns (bool) {
uint256 _lastUpdate = Stakes.lastUpdatedBlockNumberOfAddress(_owner);
uint8 _depthResponsibility = _depth - Stakes.heightOfAddress(_owner);

if (currentPhaseReveal()) {
revert WrongPhase();
}

if (_lastUpdate == 0) {
uint64 lookahead = currentPhaseClaim() ? 1 : 0;
uint256 _stake = Stakes.nodeEffectiveStakeLookahead(_owner, lookahead);

if (_stake == 0) {
revert NotStaked();
}

if (_lastUpdate >= block.number - 2 * ROUND_LENGTH) {
revert MustStake2Rounds();
}
uint8 _depthResponsibility = _depth - Stakes.heightOfAddressLookahead(_owner, lookahead);

return inProximity(Stakes.overlayOfAddress(_owner), currentRoundAnchor(), _depthResponsibility);
return
inProximity(
Stakes.overlayOfAddressLookahead(_owner, lookahead),
currentRoundAnchor(),
_depthResponsibility
);
}

// ----------------------------- Reveal ------------------------------
Expand Down
Loading
Loading