From bd9d7b4e862f0f0b1c24d4cd7f61217daa47f866 Mon Sep 17 00:00:00 2001 From: Mark Bliss Date: Wed, 30 Aug 2023 13:33:07 +0200 Subject: [PATCH 1/4] style: stamp (#159) --- src/PostageStamp.sol | 231 +++++++++++++++++++++++-------------------- 1 file changed, 124 insertions(+), 107 deletions(-) diff --git a/src/PostageStamp.sol b/src/PostageStamp.sol index 0257a626..2ee2a9ab 100644 --- a/src/PostageStamp.sol +++ b/src/PostageStamp.sol @@ -28,35 +28,7 @@ import "./OrderStatisticsTree/HitchensOrderStatisticsTreeLib.sol"; contract PostageStamp is AccessControl, Pausable { using HitchensOrderStatisticsTreeLib for HitchensOrderStatisticsTreeLib.Tree; - /** - * @dev Emitted when a new batch is created. - */ - event BatchCreated( - bytes32 indexed batchId, - uint256 totalAmount, - uint256 normalisedBalance, - address owner, - uint8 depth, - uint8 bucketDepth, - bool immutableFlag - ); - - event PotWithdrawn(address recipient, uint256 totalAmount); - - /** - * @dev Emitted when an existing batch is topped up. - */ - event BatchTopUp(bytes32 indexed batchId, uint256 topupAmount, uint256 normalisedBalance); - - /** - * @dev Emitted when the depth of an existing batch increases. - */ - event BatchDepthIncrease(bytes32 indexed batchId, uint8 newDepth, uint256 normalisedBalance); - - /** - *@dev Emitted on every price update. - */ - event PriceUpdate(uint256 price); + // ----------------------------- Type declarations ------------------------------ struct Batch { // Owner of this batch (0 if not valid). @@ -73,6 +45,8 @@ contract PostageStamp is AccessControl, Pausable { uint256 lastUpdatedBlockNumber; } + // ----------------------------- State variables ------------------------------ + // Role allowed to increase totalOutPayment. bytes32 public constant PRICE_ORACLE_ROLE = keccak256("PRICE_ORACLE"); // Role allowed to pause @@ -110,6 +84,43 @@ contract PostageStamp is AccessControl, Pausable { // Normalised balance at the blockheight expire() was last called. uint256 public lastExpiryBalance; + // ----------------------------- Events ------------------------------ + + /** + * @dev Emitted when a new batch is created. + */ + event BatchCreated( + bytes32 indexed batchId, + uint256 totalAmount, + uint256 normalisedBalance, + address owner, + uint8 depth, + uint8 bucketDepth, + bool immutableFlag + ); + + /** + * @dev Emitted when an pot is Withdrawn. + */ + event PotWithdrawn(address recipient, uint256 totalAmount); + + /** + * @dev Emitted when an existing batch is topped up. + */ + event BatchTopUp(bytes32 indexed batchId, uint256 topupAmount, uint256 normalisedBalance); + + /** + * @dev Emitted when the depth of an existing batch increases. + */ + event BatchDepthIncrease(bytes32 indexed batchId, uint8 newDepth, uint256 normalisedBalance); + + /** + *@dev Emitted on every price update. + */ + event PriceUpdate(uint256 price); + + // ----------------------------- CONSTRUCTOR ------------------------------ + /** * @param _bzzToken The ERC20 token address to reference in this contract. * @param _minimumBucketDepth The minimum bucket depth of batches that can be purchased. @@ -121,6 +132,10 @@ contract PostageStamp is AccessControl, Pausable { _setupRole(PAUSER_ROLE, msg.sender); } + //////////////////////////////////////// + // SETTERS // + //////////////////////////////////////// + /** * @notice Create a new batch. * @dev At least `_initialBalancePerChunk*2^depth` tokens must be approved in the ERC20 token contract. @@ -159,6 +174,7 @@ contract PostageStamp is AccessControl, Pausable { // since the block the contract was deployed, so we must supplement this batch's // _initialBalancePerChunk with the currentTotalOutPayment() uint256 normalisedBalance = currentTotalOutPayment() + (_initialBalancePerChunk); + require(normalisedBalance > 0, "normalisedBalance cannot be zero"); //update validChunkCount to remove currently expired batches expireLimited(type(uint256).max); @@ -175,8 +191,6 @@ contract PostageStamp is AccessControl, Pausable { lastUpdatedBlockNumber: block.number }); - require(normalisedBalance > 0, "normalisedBalance cannot be zero"); - // insert into the ordered tree tree.insert(batchId, normalisedBalance); @@ -210,6 +224,7 @@ contract PostageStamp is AccessControl, Pausable { require(ERC20(bzzToken).transferFrom(msg.sender, address(this), totalAmount), "failed transfer"); uint256 normalisedBalance = currentTotalOutPayment() + (_initialBalancePerChunk); + require(normalisedBalance > 0, "normalisedBalance cannot be zero"); validChunkCount += 1 << _depth; @@ -222,8 +237,6 @@ contract PostageStamp is AccessControl, Pausable { lastUpdatedBlockNumber: block.number }); - require(normalisedBalance > 0, "normalisedBalance cannot be zero"); - tree.insert(_batchId, normalisedBalance); emit BatchCreated(_batchId, totalAmount, normalisedBalance, _owner, _depth, _bucketDepth, _immutable); @@ -294,19 +307,6 @@ contract PostageStamp is AccessControl, Pausable { emit BatchDepthIncrease(_batchId, _newDepth, batch.normalisedBalance); } - /** - * @notice Return the per chunk balance not yet used up. - * @param _batchId The id of an existing batch. - */ - function remainingBalance(bytes32 _batchId) public view returns (uint256) { - Batch storage batch = batches[_batchId]; - require(batch.owner != address(0), "batch does not exist or expired"); - if (batch.normalisedBalance <= currentTotalOutPayment()) { - return 0; - } - return batch.normalisedBalance - currentTotalOutPayment(); - } - /** * @notice Set a new price. * @dev Can only be called by the price oracle role. @@ -333,57 +333,6 @@ contract PostageStamp is AccessControl, Pausable { minimumValidityBlocks = _value; } - /** - * @notice Total per-chunk cost since the contract's deployment. - * @dev Returns the total normalised all-time per chunk payout. - * Only Batches with a normalised balance greater than this are valid. - */ - function currentTotalOutPayment() public view returns (uint256) { - uint256 blocks = block.number - lastUpdatedBlock; - uint256 increaseSinceLastUpdate = lastPrice * (blocks); - return totalOutPayment + (increaseSinceLastUpdate); - } - - function minimumInitialBalancePerChunk() public view returns (uint256) { - return minimumValidityBlocks * lastPrice; - } - - /** - * @notice Pause the contract. - * @dev Can only be called by the pauser when not paused. - * The contract can be provably stopped by renouncing the pauser role and the admin role once paused. - */ - function pause() public { - require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can pause"); - _pause(); - } - - /** - * @notice Unpause the contract. - * @dev Can only be called by the pauser role while paused. - */ - function unPause() public { - require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can unpause"); - _unpause(); - } - - /** - * @notice Return true if no batches exist - */ - function isBatchesTreeEmpty() public view returns (bool) { - return tree.count() == 0; - } - - /** - * @notice Get the first batch id ordered by ascending normalised balance. - * @dev If more than one batch id, return index at 0, if no batches, revert. - */ - function firstBatchId() public view returns (bytes32) { - uint256 val = tree.first(); - require(val > 0, "no batches exist"); - return tree.valueKeyAtIndex(val, 0); - } - /** * @notice Reclaims a limited number of expired batches * @dev Can be used if reclaiming all expired batches would exceed the block gas limit, causing other @@ -436,16 +385,6 @@ contract PostageStamp is AccessControl, Pausable { pot += validChunkCount * (lastExpiryBalance - leb); } - /** - * @notice Indicates whether expired batches exist. - */ - function expiredBatchesExist() public view returns (bool) { - if (isBatchesTreeEmpty()) { - return false; - } - return (remainingBalance(firstBatchId()) <= 0); - } - /** * @notice The current pot. */ @@ -469,6 +408,84 @@ contract PostageStamp is AccessControl, Pausable { pot = 0; } + /** + * @notice Pause the contract. + * @dev Can only be called by the pauser when not paused. + * The contract can be provably stopped by renouncing the pauser role and the admin role once paused. + */ + function pause() public { + require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can pause"); + _pause(); + } + + /** + * @notice Unpause the contract. + * @dev Can only be called by the pauser role while paused. + */ + function unPause() public { + require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can unpause"); + _unpause(); + } + + //////////////////////////////////////// + // GETTERS // + //////////////////////////////////////// + + /** + * @notice Total per-chunk cost since the contract's deployment. + * @dev Returns the total normalised all-time per chunk payout. + * Only Batches with a normalised balance greater than this are valid. + */ + function currentTotalOutPayment() public view returns (uint256) { + uint256 blocks = block.number - lastUpdatedBlock; + uint256 increaseSinceLastUpdate = lastPrice * (blocks); + return totalOutPayment + (increaseSinceLastUpdate); + } + + function minimumInitialBalancePerChunk() public view returns (uint256) { + return minimumValidityBlocks * lastPrice; + } + + /** + * @notice Return the per chunk balance not yet used up. + * @param _batchId The id of an existing batch. + */ + function remainingBalance(bytes32 _batchId) public view returns (uint256) { + Batch storage batch = batches[_batchId]; + require(batch.owner != address(0), "batch does not exist or expired"); + if (batch.normalisedBalance <= currentTotalOutPayment()) { + return 0; + } + return batch.normalisedBalance - currentTotalOutPayment(); + } + + /** + * @notice Indicates whether expired batches exist. + */ + function expiredBatchesExist() public view returns (bool) { + if (isBatchesTreeEmpty()) { + return false; + } + return (remainingBalance(firstBatchId()) <= 0); + } + + /** + * @notice Return true if no batches exist + */ + function isBatchesTreeEmpty() public view returns (bool) { + return tree.count() == 0; + } + + /** + * @notice Get the first batch id ordered by ascending normalised balance. + * @dev If more than one batch id, return index at 0, if no batches, revert. + */ + function firstBatchId() public view returns (bytes32) { + uint256 val = tree.first(); + require(val > 0, "no batches exist"); + return tree.valueKeyAtIndex(val, 0); + } + function batchOwner(bytes32 _batchId) public view returns (address) { return batches[_batchId].owner; } From 6ef2246a66d29d627e87a36cdd91c13da13aa4fb Mon Sep 17 00:00:00 2001 From: Daniel <48453683+danieliniguezv@users.noreply.github.com> Date: Wed, 30 Aug 2023 05:50:20 -0600 Subject: [PATCH 2/4] fix: typo-on-natspec (#179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Iñiguez --- src/PostageStamp.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PostageStamp.sol b/src/PostageStamp.sol index 2ee2a9ab..5959103c 100644 --- a/src/PostageStamp.sol +++ b/src/PostageStamp.sol @@ -198,7 +198,7 @@ contract PostageStamp is AccessControl, Pausable { } /** - * @notice Manually create a new batch when faciliatating migration, can only be called by the Admin role. + * @notice Manually create a new batch when facilitating migration, can only be called by the Admin role. * @dev At least `_initialBalancePerChunk*2^depth` tokens must be approved in the ERC20 token contract. * @param _owner Owner of the new batch. * @param _initialBalancePerChunk Initial balance per chunk of the batch. From 5b1a3737b9df3885a1a851155314f5ad3040c329 Mon Sep 17 00:00:00 2001 From: Mark Bliss Date: Wed, 30 Aug 2023 13:50:47 +0200 Subject: [PATCH 3/4] style: oracle (#160) --- src/PriceOracle.sol | 54 +++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/PriceOracle.sol b/src/PriceOracle.sol index 0c079e05..4eebf01e 100644 --- a/src/PriceOracle.sol +++ b/src/PriceOracle.sol @@ -10,10 +10,7 @@ import "./interface/IPostageStamp.sol"; */ contract PriceOracle is AccessControl { - /** - *@dev Emitted on every price update. - */ - event PriceUpdate(uint256 price); + // ----------------------------- State variables ------------------------------ // Role allowed to update price bytes32 public constant PRICE_UPDATER_ROLE = keccak256("PRICE_UPDATER"); @@ -36,11 +33,24 @@ contract PriceOracle is AccessControl { // The address of the linked PostageStamp contract IPostageStamp public postageStamp; + // ----------------------------- Events ------------------------------ + + /** + *@dev Emitted on every price update. + */ + event PriceUpdate(uint256 price); + + // ----------------------------- CONSTRUCTOR ------------------------------ + constructor(address _postageStamp, address multisig) { _setupRole(DEFAULT_ADMIN_ROLE, multisig); postageStamp = IPostageStamp(_postageStamp); } + //////////////////////////////////////// + // SETTERS // + //////////////////////////////////////// + /** * @notice Manually set the price. * @dev Can only be called by the admin role. @@ -59,24 +69,6 @@ contract PriceOracle is AccessControl { emit PriceUpdate(currentPrice); } - /** - * @notice Pause the contract. - * @dev Can only be called by the admin role. - */ - function pause() external { - require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not the admin"); - isPaused = true; - } - - /** - * @notice Unpause the contract. - * @dev Can only be called by the admin role. - */ - function unPause() external { - require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not the admin"); - isPaused = false; - } - /** * @notice Automatically adjusts the price, called from the Redistribution contract * @dev The ideal redundancy in Swarm is 4 nodes per neighbourhood. Each round, the @@ -126,4 +118,22 @@ contract PriceOracle is AccessControl { emit PriceUpdate(currentPrice); } } + + /** + * @notice Pause the contract. + * @dev Can only be called by the admin role. + */ + function pause() external { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not the admin"); + isPaused = true; + } + + /** + * @notice Unpause the contract. + * @dev Can only be called by the admin role. + */ + function unPause() external { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not the admin"); + isPaused = false; + } } From acab296b08a988bc683d2927d4a6340f1baca158 Mon Sep 17 00:00:00 2001 From: Mark Bliss Date: Wed, 30 Aug 2023 13:51:14 +0200 Subject: [PATCH 4/4] style: staking (#161) --- src/Staking.sol | 157 +++++++++++++++++++++++++++--------------------- 1 file changed, 87 insertions(+), 70 deletions(-) diff --git a/src/Staking.sol b/src/Staking.sol index 21012549..f10b42ab 100644 --- a/src/Staking.sol +++ b/src/Staking.sol @@ -14,20 +14,7 @@ import "@openzeppelin/contracts/security/Pausable.sol"; */ contract StakeRegistry is AccessControl, Pausable { - /** - * @dev Emitted when a stake is created or updated by `owner` of the `overlay` by `stakeamount`, during `lastUpdatedBlock`. - */ - event StakeUpdated(bytes32 indexed overlay, uint256 stakeAmount, address owner, uint256 lastUpdatedBlock); - - /** - * @dev Emitted when a stake for overlay `slashed` is slashed by `amount`. - */ - event StakeSlashed(bytes32 slashed, uint256 amount); - - /** - * @dev Emitted when a stake for overlay `frozen` for `time` blocks. - */ - event StakeFrozen(bytes32 slashed, uint256 time); + // ----------------------------- Type declarations ------------------------------ struct Stake { // Overlay of the node that is being staked @@ -42,6 +29,8 @@ contract StakeRegistry is AccessControl, Pausable { bool isValue; } + // ----------------------------- State variables ------------------------------ + // Associate every stake id with overlay data. mapping(bytes32 => Stake) public stakes; @@ -56,75 +45,39 @@ contract StakeRegistry is AccessControl, Pausable { // Address of the staked ERC20 token address public bzzToken; - /** - * @param _bzzToken Address of the staked ERC20 token - * @param _NetworkId Swarm network ID - */ - constructor(address _bzzToken, uint64 _NetworkId, address multisig) { - NetworkId = _NetworkId; - bzzToken = _bzzToken; - _setupRole(DEFAULT_ADMIN_ROLE, multisig); - _setupRole(PAUSER_ROLE, msg.sender); - } + // ----------------------------- Events ------------------------------ /** - * @dev Checks to see if `overlay` is frozen. - * @param overlay Overlay of staked overlay - * - * Returns a boolean value indicating whether the operation succeeded. + * @dev Emitted when a stake is created or updated by `owner` of the `overlay` by `stakeamount`, during `lastUpdatedBlock`. */ - function overlayNotFrozen(bytes32 overlay) internal view returns (bool) { - return stakes[overlay].lastUpdatedBlockNumber < block.number; - } + event StakeUpdated(bytes32 indexed overlay, uint256 stakeAmount, address owner, uint256 lastUpdatedBlock); /** - * @dev Returns the current `stakeAmount` of `overlay`. - * @param overlay Overlay of node + * @dev Emitted when a stake for overlay `slashed` is slashed by `amount`. */ - function stakeOfOverlay(bytes32 overlay) public view returns (uint256) { - return stakes[overlay].stakeAmount; - } + event StakeSlashed(bytes32 slashed, uint256 amount); /** - * @dev Returns the current usable `stakeAmount` of `overlay`. - * Checks whether the stake is currently frozen. - * @param overlay Overlay of node + * @dev Emitted when a stake for overlay `frozen` for `time` blocks. */ - function usableStakeOfOverlay(bytes32 overlay) public view returns (uint256) { - return overlayNotFrozen(overlay) ? stakes[overlay].stakeAmount : 0; - } + event StakeFrozen(bytes32 slashed, uint256 time); - /** - * @dev Returns the `lastUpdatedBlockNumber` of `overlay`. - */ - function lastUpdatedBlockNumberOfOverlay(bytes32 overlay) public view returns (uint256) { - return stakes[overlay].lastUpdatedBlockNumber; - } + // ----------------------------- CONSTRUCTOR ------------------------------ /** - * @dev Returns the eth address of the owner of `overlay`. - * @param overlay Overlay of node + * @param _bzzToken Address of the staked ERC20 token + * @param _NetworkId Swarm network ID */ - function ownerOfOverlay(bytes32 overlay) public view returns (address) { - return stakes[overlay].owner; + constructor(address _bzzToken, uint64 _NetworkId, address multisig) { + NetworkId = _NetworkId; + bzzToken = _bzzToken; + _setupRole(DEFAULT_ADMIN_ROLE, multisig); + _setupRole(PAUSER_ROLE, msg.sender); } - /** - * @dev Please both Endians 🥚. - * @param input Eth address used for overlay calculation. - */ - function reverse(uint64 input) internal pure returns (uint64 v) { - v = input; - - // swap bytes - v = ((v & 0xFF00FF00FF00FF00) >> 8) | ((v & 0x00FF00FF00FF00FF) << 8); - - // swap 2-byte long pairs - v = ((v & 0xFFFF0000FFFF0000) >> 16) | ((v & 0x0000FFFF0000FFFF) << 16); - - // swap 4-byte long pairs - v = (v >> 32) | (v << 32); - } + //////////////////////////////////////// + // SETTERS // + //////////////////////////////////////// /** * @notice Create a new stake or update an existing one. @@ -190,8 +143,8 @@ contract StakeRegistry is AccessControl, Pausable { require(hasRole(REDISTRIBUTOR_ROLE, msg.sender), "only redistributor can freeze stake"); if (stakes[overlay].isValue) { - emit StakeFrozen(overlay, time); stakes[overlay].lastUpdatedBlockNumber = block.number + time; + emit StakeFrozen(overlay, time); } } @@ -202,7 +155,7 @@ contract StakeRegistry is AccessControl, Pausable { */ function slashDeposit(bytes32 overlay, uint256 amount) external { require(hasRole(REDISTRIBUTOR_ROLE, msg.sender), "only redistributor can slash stake"); - emit StakeSlashed(overlay, amount); + if (stakes[overlay].isValue) { if (stakes[overlay].stakeAmount > amount) { stakes[overlay].stakeAmount -= amount; @@ -210,6 +163,7 @@ contract StakeRegistry is AccessControl, Pausable { } else { delete stakes[overlay]; } + emit StakeSlashed(overlay, amount); } } @@ -229,4 +183,67 @@ contract StakeRegistry is AccessControl, Pausable { require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can unpause"); _unpause(); } + + //////////////////////////////////////// + // GETTERS // + //////////////////////////////////////// + + /** + * @dev Checks to see if `overlay` is frozen. + * @param overlay Overlay of staked overlay + * + * Returns a boolean value indicating whether the operation succeeded. + */ + function overlayNotFrozen(bytes32 overlay) internal view returns (bool) { + return stakes[overlay].lastUpdatedBlockNumber < block.number; + } + + /** + * @dev Returns the current `stakeAmount` of `overlay`. + * @param overlay Overlay of node + */ + function stakeOfOverlay(bytes32 overlay) public view returns (uint256) { + return stakes[overlay].stakeAmount; + } + + /** + * @dev Returns the current usable `stakeAmount` of `overlay`. + * Checks whether the stake is currently frozen. + * @param overlay Overlay of node + */ + function usableStakeOfOverlay(bytes32 overlay) public view returns (uint256) { + return overlayNotFrozen(overlay) ? stakes[overlay].stakeAmount : 0; + } + + /** + * @dev Returns the `lastUpdatedBlockNumber` of `overlay`. + */ + function lastUpdatedBlockNumberOfOverlay(bytes32 overlay) public view returns (uint256) { + return stakes[overlay].lastUpdatedBlockNumber; + } + + /** + * @dev Returns the eth address of the owner of `overlay`. + * @param overlay Overlay of node + */ + function ownerOfOverlay(bytes32 overlay) public view returns (address) { + return stakes[overlay].owner; + } + + /** + * @dev Please both Endians 🥚. + * @param input Eth address used for overlay calculation. + */ + function reverse(uint64 input) internal pure returns (uint64 v) { + v = input; + + // swap bytes + v = ((v & 0xFF00FF00FF00FF00) >> 8) | ((v & 0x00FF00FF00FF00FF) << 8); + + // swap 2-byte long pairs + v = ((v & 0xFFFF0000FFFF0000) >> 16) | ((v & 0x0000FFFF0000FFFF) << 16); + + // swap 4-byte long pairs + v = (v >> 32) | (v << 32); + } }