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

add admin only functions #2512

Merged
merged 5 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
72 changes: 65 additions & 7 deletions contracts/src/StakeTable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ contract StakeTable is AbstractStakeTable {
// Error raised when zero point keys are provided
error NoKeyChange();

/// Error raised when the caller is not the admin
error Unauthorized();

/// Error raised when the light client address is invalid
error InvalidAddress();

/// Error raised when the value is invalid
error InvalidValue();

/// Mapping from a hash of a BLS key to a node struct defined in the abstract contract.
mapping(address account => Node node) public nodes;

Expand Down Expand Up @@ -92,7 +101,17 @@ contract StakeTable is AbstractStakeTable {

uint64 public maxChurnRate;

constructor(address _tokenAddress, address _lightClientAddress, uint64 churnRate) {
uint256 public minStakeAmount;

address public admin;

/// TODO change constructor to initialize function when we make the contract upgradeable
constructor(
address _tokenAddress,
address _lightClientAddress,
uint64 churnRate,
uint256 _minStakeAmount
) {
tokenAddress = _tokenAddress;
lightClient = LightClient(_lightClientAddress);

Expand All @@ -105,6 +124,10 @@ contract StakeTable is AbstractStakeTable {
// It is not possible to exit during the first epoch.
firstAvailableExitEpoch = 1;
_numPendingExits = 0;

minStakeAmount = _minStakeAmount;

admin = msg.sender;
}

/// @dev Computes a hash value of some G2 point.
Expand Down Expand Up @@ -269,7 +292,7 @@ contract StakeTable is AbstractStakeTable {
BN254.G1Point memory blsSig,
uint64 validUntilEpoch
) external override {
uint256 fixedStakeAmount = minStakeAmount();
uint256 fixedStakeAmount = minStakeAmount;

// Verify that the sender amount is the minStakeAmount
if (amount < fixedStakeAmount) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have this amount variable if we are not using it? Everyone puts in the same amount (minStakeAmount) when calling register().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for pointing this out, confirming with @veilgets. Are we doing a fixedAmount and thus the amount they stake has to be that fixedAmount or are we doing a variable amount that must be >= some minAmount?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're choosing to go with a variable stake, so updated here 2feda04

Expand Down Expand Up @@ -392,6 +415,8 @@ contract StakeTable is AbstractStakeTable {
/// @notice Request to exit from the stake table, not immediately withdrawable!
///
/// @dev TODO modify this according to the current spec
/// @dev TODO add a function to check if the node is eligible to exit
/// @dev TODO discuss if we want an admin administered emergency exit
function requestExit() external override {
Node memory node = nodes[msg.sender];

Expand Down Expand Up @@ -524,10 +549,43 @@ contract StakeTable is AbstractStakeTable {
emit UpdatedConsensusKeys(msg.sender, node.blsVK, node.schnorrVK);
}

/// @notice Minimum stake amount
/// @return Minimum stake amount
/// TODO: This value should be a variable modifiable by admin
function minStakeAmount() public pure returns (uint256) {
return 10 ether;
/// @notice Update the admin
/// @dev The admin cannot be set to the zero address
/// @param _admin The new admin
function updateAdmin(address _admin) external {
if (msg.sender != admin) revert Unauthorized();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a conscientious decision not to inherit from an existing contract with access control?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you know what, i was thinking through it, wondered if this was more gas efficient

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switched to Ownable here dbcfeb3

if (_admin == address(0)) revert InvalidAddress();
admin = _admin;
emit AdminUpdated(admin);
}

/// @notice Update the min stake amount
/// @dev The min stake amount cannot be set to zero
/// @param _minStakeAmount The new min stake amount
function updateMinStakeAmount(uint256 _minStakeAmount) external {
if (msg.sender != admin) revert Unauthorized();
if (_minStakeAmount == 0) revert InvalidValue();
minStakeAmount = _minStakeAmount;
emit MinStakeAmountUpdated(minStakeAmount);
}

/// @notice Update the max churn rate
/// @dev The max churn rate cannot be set to zero
/// @param _maxChurnRate The new max churn rate
function updateMaxChurnRate(uint64 _maxChurnRate) external {
if (msg.sender != admin) revert Unauthorized();
if (_maxChurnRate == 0) revert InvalidValue();
maxChurnRate = _maxChurnRate;
emit MaxChurnRateUpdated(maxChurnRate);
}

/// @notice Update the light client address
/// @dev The light client address cannot be set to the zero address
/// @param _lightClientAddress The new light client address
function updateLightClientAddress(address _lightClientAddress) external {
if (msg.sender != admin) revert Unauthorized();
if (_lightClientAddress == address(0)) revert InvalidAddress();
lightClient = LightClient(_lightClientAddress);
emit LightClientAddressUpdated(_lightClientAddress);
}
}
16 changes: 16 additions & 0 deletions contracts/src/interfaces/AbstractStakeTable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ abstract contract AbstractStakeTable {
address account, BN254.G2Point newBlsVK, EdOnBN254.EdOnBN254Point newSchnorrVK
);

/// @notice Signals the admin has been updated
/// @param admin the new admin
event AdminUpdated(address admin);

/// @notice Signals the min stake amount has been updated
/// @param minStakeAmount the new min stake amount
event MinStakeAmountUpdated(uint256 minStakeAmount);

/// @notice Signals the max churn rate has been updated
/// @param maxChurnRate the new max churn rate
event MaxChurnRateUpdated(uint256 maxChurnRate);

/// @notice Signals the light client address has been updated
/// @param lightClientAddress the new light client address
event LightClientAddressUpdated(address lightClientAddress);

/// @dev (sadly, Solidity doesn't support type alias on non-primitive types)
// We avoid declaring another struct even if the type info helps with readability,
// extra layer of struct introduces overhead and more gas cost.
Expand Down
79 changes: 78 additions & 1 deletion contracts/test/StakeTable.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ contract StakeTable_register_Test is Test {
ExampleToken public token;
LightClientMock public lcMock;
uint256 public constant INITIAL_BALANCE = 10 ether;
uint256 public constant MIN_STAKE_AMOUNT = 10 ether;
address public exampleTokenCreator;

function genClientWallet(address sender, string memory seed)
Expand Down Expand Up @@ -77,7 +78,7 @@ contract StakeTable_register_Test is Test {

lcMock = new LightClientMock(genesis, genesisStakeTableState, 864000);
address lightClientAddress = address(lcMock);
stakeTable = new S(address(token), lightClientAddress, 10);
stakeTable = new S(address(token), lightClientAddress, 10, MIN_STAKE_AMOUNT);
}

function testFuzz_RevertWhen_InvalidBLSSig(uint256 scalar) external {
Expand Down Expand Up @@ -745,4 +746,80 @@ contract StakeTable_register_Test is Test {
stakeTable.withdrawFunds();
vm.stopPrank();
}

// test set admin succeeds
function test_setAdmin_succeeds() public {
vm.expectEmit(false, false, false, true, address(stakeTable));
emit AbstractStakeTable.AdminUpdated(makeAddr("admin"));
stakeTable.updateAdmin(makeAddr("admin"));
assertEq(stakeTable.admin(), makeAddr("admin"));
}

// test set admin fails if not admin or invalid admin address
function test_revertWhen_setAdmin_NotAdminOrInvalidAdminAddress() public {
vm.startPrank(makeAddr("randomUser"));
vm.expectRevert(S.Unauthorized.selector);
stakeTable.updateAdmin(makeAddr("admin"));
vm.stopPrank();

vm.expectRevert(S.InvalidAddress.selector);
stakeTable.updateAdmin(address(0));
}

// test update min stake amount succeeds
function test_updateMinStakeAmount_succeeds() public {
vm.expectEmit(false, false, false, true, address(stakeTable));
emit AbstractStakeTable.MinStakeAmountUpdated(10 ether);
stakeTable.updateMinStakeAmount(10 ether);
assertEq(stakeTable.minStakeAmount(), 10 ether);
}

// test update min stake amount fails if not admin or invalid stake amount
function test_revertWhen_updateMinStakeAmount_NotAdminOrInvalidStakeAmount() public {
vm.startPrank(makeAddr("randomUser"));
vm.expectRevert(S.Unauthorized.selector);
stakeTable.updateMinStakeAmount(10 ether);
vm.stopPrank();

vm.expectRevert(S.InvalidValue.selector);
stakeTable.updateMinStakeAmount(0);
}

// test update max churn rate succeeds
function test_updateMaxChurnRate_succeeds() public {
vm.expectEmit(false, false, false, true, address(stakeTable));
emit AbstractStakeTable.MaxChurnRateUpdated(10);
stakeTable.updateMaxChurnRate(10);
assertEq(stakeTable.maxChurnRate(), 10);
}

// test update max churn rate fails if not admin or invalid churn amount
function test_revertWhen_updateMaxChurnRate_NotAdminOrInvalidChurnAmount() public {
vm.startPrank(makeAddr("randomUser"));
vm.expectRevert(S.Unauthorized.selector);
stakeTable.updateMaxChurnRate(10);
vm.stopPrank();

vm.expectRevert(S.InvalidValue.selector);
stakeTable.updateMaxChurnRate(0);
}

// test update light client address succeeds
function test_updateLightClientAddress_succeeds() public {
vm.expectEmit(false, false, false, true, address(stakeTable));
emit AbstractStakeTable.LightClientAddressUpdated(makeAddr("lightClient"));
stakeTable.updateLightClientAddress(makeAddr("lightClient"));
assertEq(address(stakeTable.lightClient()), makeAddr("lightClient"));
}

// test update light client address fails if not admin or bad address
function test_revertWhen_updateLightClientAddress_NotAdminOrBadAddress() public {
vm.startPrank(makeAddr("randomUser"));
vm.expectRevert(S.Unauthorized.selector);
stakeTable.updateLightClientAddress(makeAddr("lightClient"));
vm.stopPrank();

vm.expectRevert(S.InvalidAddress.selector);
stakeTable.updateLightClientAddress(address(0));
}
}
Loading