Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved staking (new staking - part 2 - SWIP-20) #264

Merged
merged 41 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1f31392
rename to commitedStake
0xCardinalError May 30, 2024
bd828e6
add potential stake and commited stake and update their amounts with …
0xCardinalError May 30, 2024
75280ae
update from latest staking
0xCardinalError Jun 10, 2024
c16f56c
add potential and commited stakes
0xCardinalError Jun 10, 2024
40e6d0e
change to potentialStake
0xCardinalError Jun 10, 2024
cbcac28
add effectiveStake as calculation
0xCardinalError Jun 10, 2024
63d4081
remove minimum from redis and add it to staking
0xCardinalError Jun 12, 2024
dccbe4d
use just 2 params
0xCardinalError Jun 13, 2024
658d9c0
substract commitedStake amount accordingly to currentPrice
0xCardinalError Jun 13, 2024
ae07e41
make new deployment procedure with new staking contracts
0xCardinalError Jun 24, 2024
fd1cd1a
add check if node has entered staking game
0xCardinalError Jun 24, 2024
aa1d34e
fix redistribution tests to match new staking calculations
0xCardinalError Jun 24, 2024
f90d7b7
add migration code, fix staking code
0xCardinalError Jun 24, 2024
f1e9bbc
remove logs
0xCardinalError Jun 24, 2024
03b5d86
add migration tests that checks values
0xCardinalError Jul 2, 2024
2dfab4e
fix error in value changes, make withdrawl test
0xCardinalError Jul 2, 2024
f838e56
add few more tests
0xCardinalError Jul 5, 2024
e0ce4cd
add extra case in test, for minimal first deposit
0xCardinalError Jul 5, 2024
a50e039
fix wrong spelling of committed in many places
0xCardinalError Jul 8, 2024
7209d9d
group variable
0xCardinalError Jul 8, 2024
5ed3b21
remove obsolete code in calc
0xCardinalError Jul 8, 2024
f399af6
remove obsolete variable, add comment about it
0xCardinalError Jul 8, 2024
094c116
better comments
0xCardinalError Jul 8, 2024
b8fb6bb
remove not needed struct access
0xCardinalError Jul 8, 2024
c749d83
change naming
0xCardinalError Jul 9, 2024
5998c9c
add withdrawable stake read function
0xCardinalError Jul 9, 2024
ed004d2
change usable stake to effective stake
0xCardinalError Jul 11, 2024
0b6d0a7
fix stat test with effective stake
0xCardinalError Jul 12, 2024
8aad41a
add test for multiple withdrawls
0xCardinalError Jul 12, 2024
dae4e65
remove wrong line
0xCardinalError Jul 12, 2024
01ea1f1
add check that value is the same
0xCardinalError Jul 12, 2024
1b64dba
add more tests and conditions
0xCardinalError Jul 12, 2024
9e3e1dd
optmize gas
0xCardinalError Jul 16, 2024
ded87e6
another optimization for gas with local var
0xCardinalError Jul 16, 2024
3438f2d
fix wrong var
0xCardinalError Jul 16, 2024
974b71f
Merge branch 'new_staking' of https://github.com/ethersphere/storage-…
0xCardinalError Jul 16, 2024
b06a313
deploy to tesnet light and fix deployment constructor params
0xCardinalError Jul 16, 2024
849937d
remove non exsisting call
0xCardinalError Jul 22, 2024
4fb1f33
testnet deployed
0xCardinalError Aug 22, 2024
6021b5c
fix: Remove pauser role (#263)
0xCardinalError Aug 26, 2024
228946c
fix: remove isValue as it is not needed (#260)
0xCardinalError Aug 26, 2024
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
3 changes: 2 additions & 1 deletion deploy/local/003_deploy_staking.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { DeployFunction } from 'hardhat-deploy/types';
import { networkConfig } from '../../helper-hardhat-config';

const func: DeployFunction = async function ({ deployments, getNamedAccounts, network, ethers }) {

Check warning on line 4 in deploy/local/003_deploy_staking.ts

View workflow job for this annotation

GitHub Actions / check

'ethers' is defined but never used
const { deploy, get, log } = deployments;
const { deployer } = await getNamedAccounts();
const swarmNetworkID = networkConfig[network.name]?.swarmNetworkId;

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

const args = [token.address, swarmNetworkID];
const args = [token.address, swarmNetworkID, oracleAddress];
await deploy('StakeRegistry', {
from: deployer,
args: args,
Expand Down
14 changes: 8 additions & 6 deletions deploy/main/003_deploy_staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ const func: DeployFunction = async function ({ deployments, getNamedAccounts, ne
const { deployer } = await getNamedAccounts();
const swarmNetworkID = networkConfig[network.name]?.swarmNetworkId;
const token = await get('Token');
let staking = null;
const oracleAddress = (await get('PriceOracle')).address;

// We use legacy token that was migrated, until we deploy new one with this framework
if (!(staking = await get('StakeRegistry'))) {
} else {
log('Using already deployed Staking at', staking.address);
}
const args = [token.address, swarmNetworkID, oracleAddress];
await deploy('StakeRegistry', {
from: deployer,
args: args,
log: true,
waitConfirmations: networkConfig[network.name]?.blockConfirmations || 6,
});

log('----------------------------------------------------');
};
Expand Down
3 changes: 2 additions & 1 deletion deploy/test/003_deploy_staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ const func: DeployFunction = async function ({ deployments, getNamedAccounts, ne
const { deployer } = await getNamedAccounts();
const swarmNetworkID = networkConfig[network.name]?.swarmNetworkId;
const token = await get('TestToken');
const oracleAddress = (await get('PriceOracle')).address;

const args = [token.address, swarmNetworkID];
const args = [token.address, swarmNetworkID, oracleAddress];
await deploy('StakeRegistry', {
from: deployer,
args: args,
Expand Down
26 changes: 10 additions & 16 deletions src/Redistribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface IStakeRegistry {

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

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

function getStakeStruct(address _owner) external view returns (Stake memory);
}
Expand All @@ -45,7 +45,7 @@ interface IStakeRegistry {
*
* Once the _commit_ round has elapsed, participating nodes must provide the values used to calculate their obsfucated
* _commit_ hash, which, once verified for correctness and proximity to the anchor are retained in the _currentReveals_.
* Nodes that have commited but do not reveal the correct values used to create the pre-image will have their stake
* Nodes that have committed but do not reveal the correct values used to create the pre-image will have their stake
* "frozen" for a period of rounds proportional to their reported depth.
*
* During the _reveal_ round, randomness is updated after every successful reveal. Once the reveal round is concluded,
Expand Down Expand Up @@ -155,9 +155,6 @@ contract Redistribution is AccessControl, Pausable {
// The length of a round in blocks.
uint256 private constant ROUND_LENGTH = 152;

// The miniumum stake allowed to be staked using the Staking contract.
uint64 private constant MIN_STAKE = 100000000000000000;

// Maximum value of the keccack256 hash.
bytes32 private constant MAX_H = 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff;

Expand Down Expand Up @@ -223,13 +220,13 @@ contract Redistribution is AccessControl, Pausable {
error NotCommitPhase(); // Game is not in commit phase
error NoCommitsReceived(); // Round didn't receive any commits
error PhaseLastBlock(); // We don't permit commits in last block of the phase
error BelowMinimumStake(); // Node participating in game has stake below minimum treshold
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 AlreadyCommited(); // Node already commited in this round
error AlreadyCommitted(); // Node already committed in this round
error NotRevealPhase(); // Game is not in reveal phase
error OutOfDepthReveal(bytes32); // Anchor is out of reported depth in Reveal phase, anchor data available as argument
error OutOfDepthClaim(uint8); // Anchor is out of reported depth in Claim phase, entryProof index is argument
Expand Down Expand Up @@ -290,7 +287,8 @@ contract Redistribution is AccessControl, Pausable {
function commit(bytes32 _obfuscatedHash, uint64 _roundNumber) external whenNotPaused {
uint64 cr = currentRound();
bytes32 _overlay = Stakes.overlayOfAddress(msg.sender);
uint256 _stake = Stakes.stakeOfAddress(msg.sender);
uint256 _stake = Stakes.nodeEffectiveStake(msg.sender);
uint256 _lastUpdate = Stakes.lastUpdatedBlockNumberOfAddress(msg.sender);

if (!currentPhaseCommit()) {
revert NotCommitPhase();
Expand All @@ -307,11 +305,11 @@ contract Redistribution is AccessControl, Pausable {
revert CommitRoundNotStarted();
}

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

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

Expand All @@ -326,7 +324,7 @@ contract Redistribution is AccessControl, Pausable {

for (uint256 i = 0; i < commitsArrayLength; ) {
if (currentCommits[i].overlay == _overlay) {
revert AlreadyCommited();
revert AlreadyCommitted();
}

unchecked {
Expand Down Expand Up @@ -819,10 +817,6 @@ contract Redistribution is AccessControl, Pausable {
revert MustStake2Rounds();
}

if (Stakes.stakeOfAddress(_owner) < MIN_STAKE) {
revert BelowMinimumStake();
}

return inProximity(Stakes.overlayOfAddress(_owner), currentRoundAnchor(), _depth);
}

Expand Down
121 changes: 89 additions & 32 deletions src/Staking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";

interface IPriceOracle {
function currentPrice() external view returns (uint32);
}

/**
* @title Staking contract for the Swarm storage incentives
* @author The Swarm Authors
Expand All @@ -18,8 +22,10 @@ contract StakeRegistry is AccessControl, Pausable {
struct Stake {
// Overlay of the node that is being staked
bytes32 overlay;
// Amount of tokens staked
uint256 stakeAmount;
// Stake balance expressed through price oracle
uint256 committedStake;
// Stake balance expressed in BZZ
uint256 potentialStake;
// Block height the stake was updated
uint256 lastUpdatedBlockNumber;
// Used to indicate presents in stakes struct
Expand All @@ -37,15 +43,27 @@ contract StakeRegistry is AccessControl, Pausable {
// Swarm network ID
uint64 NetworkId;

// The miniumum stake allowed to be staked using the Staking contract.
uint64 private constant MIN_STAKE = 100000000000000000;

// Address of the staked ERC20 token
address public immutable bzzToken;

// The address of the linked PriceOracle contract.
IPriceOracle public OracleContract;

// ----------------------------- Events ------------------------------

/**
* @dev Emitted when a stake is created or updated by `owner` of the `overlay` by `stakeamount`, during `lastUpdatedBlock`.
* @dev Emitted when a stake is created or updated by `owner` of the `overlay` by `committedStake`, and `potentialStake` during `lastUpdatedBlock`.
*/
event StakeUpdated(address indexed owner, uint256 stakeAmount, bytes32 overlay, uint256 lastUpdatedBlock);
event StakeUpdated(
address indexed owner,
uint256 committedStake,
uint256 potentialStake,
bytes32 overlay,
uint256 lastUpdatedBlock
);

/**
* @dev Emitted when a stake for address `slashed` is slashed by `amount`.
Expand All @@ -62,23 +80,30 @@ contract StakeRegistry is AccessControl, Pausable {
*/
event OverlayChanged(address owner, bytes32 overlay);

/**
* @dev Emitted when a stake for address is withdrawn
*/
event StakeWithdrawn(address node, uint256 amount);

// ----------------------------- Errors ------------------------------

error TransferFailed(); // Used when token transfers fail
error Frozen(); // Used when an action cannot proceed because the overlay is frozen
error Unauthorized(); // Used where only the owner can perform the action
error OnlyRedistributor(); // Used when only the redistributor role is allowed
error OnlyPauser(); // Used when only the pauser role is allowed
error BelowMinimumStake(); // Node participating in game has stake below minimum treshold

// ----------------------------- CONSTRUCTOR ------------------------------

/**
* @param _bzzToken Address of the staked ERC20 token
* @param _NetworkId Swarm network ID
*/
constructor(address _bzzToken, uint64 _NetworkId) {
constructor(address _bzzToken, uint64 _NetworkId, address _oracleContract) {
NetworkId = _NetworkId;
bzzToken = _bzzToken;
OracleContract = IPriceOracle(_oracleContract);
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(PAUSER_ROLE, msg.sender);
}
Expand All @@ -91,26 +116,34 @@ contract StakeRegistry is AccessControl, Pausable {
* @notice Create a new stake or update an existing one, change overlay of node
* @dev At least `_initialBalancePerChunk*2^depth` number of tokens need to be preapproved for this contract.
* @param _setNonce Nonce that was used for overlay calculation.
* @param _addAmount Deposited amount of ERC20 tokens.
* @param _addAmount Deposited amount of ERC20 tokens, equals to added Potential stake value
*/
function manageStake(bytes32 _setNonce, uint256 _addAmount) external whenNotPaused {
bytes32 _previousOverlay = stakes[msg.sender].overlay;
bytes32 _newOverlay = keccak256(abi.encodePacked(msg.sender, reverse(NetworkId), _setNonce));
uint256 _addCommittedStake = _addAmount / OracleContract.currentPrice(); // losing some decimals from start 10n16 becomes 99999999999984000

// First time adding stake, check the minimum is added
if (_addAmount < MIN_STAKE && !stakes[msg.sender].isValue) {
revert BelowMinimumStake();
}

if (stakes[msg.sender].isValue && !addressNotFrozen(msg.sender)) revert Frozen();
0xCardinalError marked this conversation as resolved.
Show resolved Hide resolved
uint256 updatedAmount = stakes[msg.sender].isValue ? _addAmount + stakes[msg.sender].stakeAmount : _addAmount;
uint256 updatedCommittedStake = stakes[msg.sender].committedStake + _addCommittedStake;
uint256 updatedPotentialStake = stakes[msg.sender].potentialStake + _addAmount;

stakes[msg.sender] = Stake({
overlay: _newOverlay,
stakeAmount: updatedAmount,
committedStake: updatedCommittedStake,
potentialStake: updatedPotentialStake,
lastUpdatedBlockNumber: block.number,
isValue: true
});

// Transfer tokens and emit event that stake has been updated
if (_addAmount > 0) {
if (!ERC20(bzzToken).transferFrom(msg.sender, address(this), _addAmount)) revert TransferFailed();
emit StakeUpdated(msg.sender, updatedAmount, _newOverlay, block.number);
emit StakeUpdated(msg.sender, updatedCommittedStake, updatedPotentialStake, _newOverlay, block.number);
}

// Emit overlay change event
Expand All @@ -120,23 +153,29 @@ contract StakeRegistry is AccessControl, Pausable {
}

/**
* @dev Withdraw stake only when the staking contract is paused,
* @param _amount The amount of ERC20 tokens to be withdrawn
* @dev Withdraw node stake surplus
*/
function withdrawFromStake(uint256 _amount) external whenPaused {
Stake memory stake = stakes[msg.sender];

// We cap the limit to not be over what is possible
uint256 withDrawLimit = (_amount > stake.stakeAmount) ? stake.stakeAmount : _amount;
stake.stakeAmount -= withDrawLimit;
function withdrawFromStake() external {
uint256 _surplusStake = stakes[msg.sender].potentialStake -
0xCardinalError marked this conversation as resolved.
Show resolved Hide resolved
calculateEffectiveStake(stakes[msg.sender].committedStake, stakes[msg.sender].potentialStake);

if (_surplusStake > 0) {
stakes[msg.sender].potentialStake -= _surplusStake;
if (!ERC20(bzzToken).transfer(msg.sender, _surplusStake)) revert TransferFailed();
emit StakeWithdrawn(msg.sender, _surplusStake);
}
}

if (stake.stakeAmount == 0) {
/**
* @dev Migrate stake only when the staking contract is paused,
* can only be called by the owner of the stake
*/
function migrateStake() external whenPaused {
// We take out all the stake so user can migrate stake to other contract
if (lastUpdatedBlockNumberOfAddress(msg.sender) != 0) {
if (!ERC20(bzzToken).transfer(msg.sender, stakes[msg.sender].potentialStake)) revert TransferFailed();
delete stakes[msg.sender];
} else {
stakes[msg.sender].lastUpdatedBlockNumber = block.number;
}

if (!ERC20(bzzToken).transfer(msg.sender, withDrawLimit)) revert TransferFailed();
}

/**
Expand All @@ -162,8 +201,8 @@ contract StakeRegistry is AccessControl, Pausable {
if (!hasRole(REDISTRIBUTOR_ROLE, msg.sender)) revert OnlyRedistributor();

if (stakes[_owner].isValue) {
if (stakes[_owner].stakeAmount > _amount) {
stakes[_owner].stakeAmount -= _amount;
if (stakes[_owner].potentialStake > _amount) {
stakes[_owner].potentialStake -= _amount;
stakes[_owner].lastUpdatedBlockNumber = block.number;
} else {
delete stakes[_owner];
Expand Down Expand Up @@ -209,20 +248,23 @@ contract StakeRegistry is AccessControl, Pausable {
}

/**
* @dev Returns the current `stakeAmount` of `address`.
* @dev Returns the current `effectiveStake` of `address`. previously usable stake
* @param _owner _owner of node
*/
function stakeOfAddress(address _owner) public view returns (uint256) {
return stakes[_owner].stakeAmount;
function nodeEffectiveStake(address _owner) public view returns (uint256) {
return
addressNotFrozen(_owner)
? calculateEffectiveStake(stakes[_owner].committedStake, stakes[_owner].potentialStake)
: 0;
}

/**
* @dev Returns the current usable `stakeAmount` of `address`.
* Checks whether the stake is currently frozen.
* @param _owner owner of node
* @dev Check the amount that is possible to withdraw as surplus
*/
function usableStakeOfAddress(address _owner) public view returns (uint256) {
return addressNotFrozen(_owner) ? stakes[_owner].stakeAmount : 0;
function withdrawableStake() public view returns (uint256) {
return
stakes[msg.sender].potentialStake -
calculateEffectiveStake(stakes[msg.sender].committedStake, stakes[msg.sender].potentialStake);
0xCardinalError marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -240,6 +282,21 @@ contract StakeRegistry is AccessControl, Pausable {
return stakes[_owner].overlay;
}

function calculateEffectiveStake(
uint256 committedStake,
uint256 potentialStakeBalance
) internal view returns (uint256) {
// Calculate the product of committedStake and unitPrice to get price in BZZ
uint256 committedStakeBzz = committedStake * OracleContract.currentPrice();

// Return the minimum value between committedStakeBzz and potentialStakeBalance
if (committedStakeBzz < potentialStakeBalance) {
return committedStakeBzz;
} else {
return potentialStakeBalance;
}
}

/**
* @dev Please both Endians 🥚.
* @param input Eth address used for overlay calculation.
Expand Down
Loading
Loading