Skip to content

Commit

Permalink
feat: Staking optimization, code and gas golfing (#245)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xCardinalError authored Jun 10, 2024
1 parent 9814a10 commit f81aea4
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 110 deletions.
2 changes: 1 addition & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ const config: HardhatUserConfig = {
runOnCompile: false,
},
gasReporter: {
enabled: false,
enabled: true,
currency: 'USD',
gasPriceApi: 'https://api.gnosisscan.io/api?module=proxy&action=eth_gasPrice', // https://docs.gnosischain.com/tools/oracles/gas-price
token: 'GNO',
Expand Down
221 changes: 120 additions & 101 deletions src/Staking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,17 @@ 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);
// ----------------------------- State variables ------------------------------

struct Stake {
// Overlay of the node that is being staked
bytes32 overlay;
// Amount of tokens staked
uint256 stakeAmount;
// Owner of `overlay`
address owner;
// Block height the stake was updated
uint256 lastUpdatedBlockNumber;
// Owner of `overlay`
address owner;
// Used to indicate presents in stakes struct
bool isValue;
}
Expand All @@ -54,78 +41,50 @@ contract StakeRegistry is AccessControl, Pausable {
uint64 NetworkId;

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

/**
* @param _bzzToken Address of the staked ERC20 token
* @param _NetworkId Swarm network ID
*/
constructor(address _bzzToken, uint64 _NetworkId) {
NetworkId = _NetworkId;
bzzToken = _bzzToken;
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_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;
}
// ----------------------------- Errors ------------------------------

/**
* @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;
}
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

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

/**
* @dev Please both Endians 🥚.
* @param input Eth address used for overlay calculation.
* @param _bzzToken Address of the staked ERC20 token
* @param _NetworkId Swarm network ID
*/
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);
constructor(address _bzzToken, uint64 _NetworkId) {
NetworkId = _NetworkId;
bzzToken = _bzzToken;
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(PAUSER_ROLE, msg.sender);
}

////////////////////////////////////////
// STATE SETTING //
////////////////////////////////////////

/**
* @notice Create a new stake or update an existing one.
* @dev At least `_initialBalancePerChunk*2^depth` number of tokens need to be preapproved for this contract.
Expand All @@ -134,20 +93,14 @@ contract StakeRegistry is AccessControl, Pausable {
* @param amount Deposited amount of ERC20 tokens.
*/
function depositStake(address _owner, bytes32 nonce, uint256 amount) external whenNotPaused {
require(_owner == msg.sender, "only owner can update stake");
if (_owner != msg.sender) revert Unauthorized();

bytes32 overlay = keccak256(abi.encodePacked(_owner, reverse(NetworkId), nonce));

uint256 updatedAmount = amount;

if (stakes[overlay].isValue) {
require(overlayNotFrozen(overlay), "overlay currently frozen");
updatedAmount = amount + stakes[overlay].stakeAmount;
}
if (stakes[overlay].isValue && !overlayNotFrozen(overlay)) revert Frozen();
uint256 updatedAmount = stakes[overlay].isValue ? amount + stakes[overlay].stakeAmount : amount;

require(ERC20(bzzToken).transferFrom(msg.sender, address(this), amount), "failed transfer");

emit StakeUpdated(overlay, updatedAmount, _owner, block.number);
if (!ERC20(bzzToken).transferFrom(msg.sender, address(this), amount)) revert TransferFailed();

stakes[overlay] = Stake({
owner: _owner,
Expand All @@ -156,6 +109,8 @@ contract StakeRegistry is AccessControl, Pausable {
lastUpdatedBlockNumber: block.number,
isValue: true
});

emit StakeUpdated(overlay, updatedAmount, _owner, block.number);
}

/**
Expand All @@ -165,20 +120,20 @@ contract StakeRegistry is AccessControl, Pausable {
* @param amount The amount of ERC20 tokens to be withdrawn
*/
function withdrawFromStake(bytes32 overlay, uint256 amount) external whenPaused {
require(stakes[overlay].owner == msg.sender, "only owner can withdraw stake");
uint256 withDrawLimit = amount;
if (amount > stakes[overlay].stakeAmount) {
withDrawLimit = stakes[overlay].stakeAmount;
}
Stake memory stake = stakes[overlay];
if (stake.owner != msg.sender) revert Unauthorized();

if (withDrawLimit < stakes[overlay].stakeAmount) {
stakes[overlay].stakeAmount -= withDrawLimit;
stakes[overlay].lastUpdatedBlockNumber = block.number;
require(ERC20(bzzToken).transfer(msg.sender, withDrawLimit), "failed withdrawal");
} else {
// We cap the limit to not be over what is possible
uint256 withDrawLimit = (amount > stake.stakeAmount) ? stake.stakeAmount : amount;
stake.stakeAmount -= withDrawLimit;

if (stake.stakeAmount == 0) {
delete stakes[overlay];
require(ERC20(bzzToken).transfer(msg.sender, withDrawLimit), "failed withdrawal");
} else {
stakes[overlay].lastUpdatedBlockNumber = block.number;
}

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

/**
Expand All @@ -187,11 +142,11 @@ contract StakeRegistry is AccessControl, Pausable {
* @param time penalty length in blocknumbers
*/
function freezeDeposit(bytes32 overlay, uint256 time) external {
require(hasRole(REDISTRIBUTOR_ROLE, msg.sender), "only redistributor can freeze stake");
if (!hasRole(REDISTRIBUTOR_ROLE, msg.sender)) revert OnlyRedistributor();

if (stakes[overlay].isValue) {
emit StakeFrozen(overlay, time);
stakes[overlay].lastUpdatedBlockNumber = block.number + time;
emit StakeFrozen(overlay, time);
}
}

Expand All @@ -201,8 +156,8 @@ contract StakeRegistry is AccessControl, Pausable {
* @param amount the amount to be slashed
*/
function slashDeposit(bytes32 overlay, uint256 amount) external {
require(hasRole(REDISTRIBUTOR_ROLE, msg.sender), "only redistributor can slash stake");
emit StakeSlashed(overlay, amount);
if (!hasRole(REDISTRIBUTOR_ROLE, msg.sender)) revert OnlyRedistributor();

if (stakes[overlay].isValue) {
if (stakes[overlay].stakeAmount > amount) {
stakes[overlay].stakeAmount -= amount;
Expand All @@ -211,27 +166,91 @@ contract StakeRegistry is AccessControl, Pausable {
delete stakes[overlay];
}
}
emit StakeSlashed(overlay, amount);
}

function changeNetworkId(uint64 _NetworkId) external {
if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert Unauthorized();
NetworkId = _NetworkId;
}

/**
* @dev Pause the contract. The contract is provably stopped by renouncing
the pauser role and the admin role after pausing, can only be called by the `PAUSER`
*/
function pause() public {
require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can pause");
if (!hasRole(PAUSER_ROLE, msg.sender)) revert OnlyPauser();
_pause();
}

/**
* @dev Unpause the contract, can only be called by the pauser when paused
*/
function unPause() public {
require(hasRole(PAUSER_ROLE, msg.sender), "only pauser can unpause");
if (!hasRole(PAUSER_ROLE, msg.sender)) revert OnlyPauser();
_unpause();
}

function changeNetworkId(uint64 _NetworkId) external {
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "only admin can change Network ID");
NetworkId = _NetworkId;
////////////////////////////////////////
// STATE READING //
////////////////////////////////////////

/**
* @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);
}
}
17 changes: 9 additions & 8 deletions test/Staking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ const errors = {
noBalance: 'ERC20: insufficient allowance',
noZeroAddress: 'owner cannot be the zero address',
belowMinimum: 'cannot be below the minimum stake value',
onlyOwner: 'only owner can update stake',
onlyOwner: 'Unauthorized()',
},
slash: {
noRole: 'only redistributor can slash stake',
noRole: 'OnlyRedistributor()',
},
freeze: {
noRole: 'only redistributor can freeze stake',
currentlyFrozen: 'overlay currently frozen',
noRole: 'OnlyRedistributor()',
currentlyFrozen: 'Frozen()',
},
pause: {
noRole: 'only pauser can pause',
noRole: 'OnlyPauser()',
currentlyPaused: 'Pausable: paused',
notCurrentlyPaused: 'Pausable: not paused',
onlyPauseCanUnPause: 'only pauser can unpause',
onlyPauseCanUnPause: 'OnlyPauser()',
},
};

Expand Down Expand Up @@ -68,8 +68,9 @@ async function mintAndApprove(payee: string, beneficiary: string, transferAmount
describe('Staking', function () {
describe('when deploying contract', function () {
beforeEach(async function () {
stakeRegistry = await ethers.getContract('StakeRegistry');
await deployments.fixture();
stakeRegistry = await ethers.getContract('StakeRegistry');

const pauserRole = await read('StakeRegistry', 'PAUSER_ROLE');
await execute('StakeRegistry', { from: deployer }, 'grantRole', pauserRole, pauser);
});
Expand Down Expand Up @@ -355,9 +356,9 @@ describe('Staking', function () {
let updatedBlockNumber: number;

beforeEach(async function () {
await deployments.fixture();
token = await ethers.getContract('TestToken', deployer);
stakeRegistry = await ethers.getContract('StakeRegistry');
await deployments.fixture();

sr_staker_0 = await ethers.getContract('StakeRegistry', staker_0);

Expand Down

0 comments on commit f81aea4

Please sign in to comment.