Skip to content

Commit

Permalink
add admin only functions (#2512)
Browse files Browse the repository at this point in the history
* add admin only functions

* use Ownable for admin management

* remove AdminUpdated event

* register with variable stake amount

* rename test
  • Loading branch information
alysiahuggins authored Feb 7, 2025
1 parent ccfc08c commit 6014858
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 22 deletions.
75 changes: 56 additions & 19 deletions contracts/src/StakeTable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { BLSSig } from "./libraries/BLSSig.sol";
import { AbstractStakeTable } from "./interfaces/AbstractStakeTable.sol";
import { LightClient } from "../src/LightClient.sol";
import { EdOnBN254 } from "./libraries/EdOnBn254.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

using EdOnBN254 for EdOnBN254.EdOnBN254Point;

/// @title Implementation of the Stake Table interface
contract StakeTable is AbstractStakeTable {
contract StakeTable is AbstractStakeTable, Ownable {
/// Error to notify restaking is not implemented yet.
error RestakingNotImplemented();

Expand Down Expand Up @@ -64,6 +65,15 @@ contract StakeTable is AbstractStakeTable {
// Error raised when zero point keys are provided
error NoKeyChange();

/// Error raised when the caller is not the owner
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 +102,16 @@ contract StakeTable is AbstractStakeTable {

uint64 public maxChurnRate;

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

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

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

minStakeAmount = _minStakeAmount;
}

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

// Verify that the sender amount is the minStakeAmount
if (amount < fixedStakeAmount) {
if (amount < minStakeAmount) {
revert InsufficientStakeAmount(amount);
}

Expand All @@ -285,13 +304,13 @@ contract StakeTable is AbstractStakeTable {

// Verify that this contract has permissions to access the validator's stake token.
uint256 allowance = ERC20(tokenAddress).allowance(msg.sender, address(this));
if (allowance < fixedStakeAmount) {
revert InsufficientAllowance(allowance, fixedStakeAmount);
if (allowance < amount) {
revert InsufficientAllowance(allowance, amount);
}

// Verify that the validator has the balance for this stake token.
uint256 balance = ERC20(tokenAddress).balanceOf(msg.sender);
if (balance < fixedStakeAmount) {
if (balance < amount) {
revert InsufficientBalance(balance);
}

Expand Down Expand Up @@ -330,23 +349,21 @@ contract StakeTable is AbstractStakeTable {
appendRegistrationQueue(registerEpoch, queueSize);

// Transfer the stake amount of ERC20 tokens from the sender to this contract.
SafeTransferLib.safeTransferFrom(
ERC20(tokenAddress), msg.sender, address(this), fixedStakeAmount
);
SafeTransferLib.safeTransferFrom(ERC20(tokenAddress), msg.sender, address(this), amount);

// Update the total staked amount
totalStake += fixedStakeAmount;
totalStake += amount;

// Create an entry for the node.
node.account = msg.sender;
node.balance = fixedStakeAmount;
node.balance = amount;
node.blsVK = blsVK;
node.schnorrVK = schnorrVK;
node.registerEpoch = registerEpoch;

nodes[msg.sender] = node;

emit Registered(msg.sender, registerEpoch, fixedStakeAmount);
emit Registered(msg.sender, registerEpoch, amount);
}

/// @notice Deposit more stakes to registered keys
Expand Down Expand Up @@ -524,10 +541,30 @@ 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 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 onlyOwner {
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 onlyOwner {
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 onlyOwner {
if (_lightClientAddress == address(0)) revert InvalidAddress();
lightClient = LightClient(_lightClientAddress);
emit LightClientAddressUpdated(_lightClientAddress);
}
}
12 changes: 12 additions & 0 deletions contracts/src/interfaces/AbstractStakeTable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ abstract contract AbstractStakeTable {
address account, BN254.G2Point newBlsVK, EdOnBN254.EdOnBN254Point newSchnorrVK
);

/// @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
109 changes: 106 additions & 3 deletions contracts/test/StakeTable.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { EdOnBN254 } from "../src/libraries/EdOnBn254.sol";
import { AbstractStakeTable } from "../src/interfaces/AbstractStakeTable.sol";
import { LightClient } from "../src/LightClient.sol";
import { LightClientMock } from "../test/mocks/LightClientMock.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

// Token contract
import { ExampleToken } from "../src/ExampleToken.sol";
Expand All @@ -29,6 +30,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 +79,8 @@ 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, exampleTokenCreator);
}

function testFuzz_RevertWhen_InvalidBLSSig(uint256 scalar) external {
Expand Down Expand Up @@ -203,8 +206,8 @@ contract StakeTable_register_Test is Test {
vm.stopPrank();
}

function test_RevertWhen_WrongStakeAmount() external {
uint64 depositAmount = 5 ether;
function test_RevertWhen_InsufficientStakeAmount() external {
uint64 depositAmount = uint64(stakeTable.minStakeAmount()) - 1;
uint64 validUntilEpoch = 10;
string memory seed = "123";

Expand Down Expand Up @@ -745,4 +748,104 @@ contract StakeTable_register_Test is Test {
stakeTable.withdrawFunds();
vm.stopPrank();
}

// test set admin succeeds
function test_setAdmin_succeeds() public {
vm.prank(exampleTokenCreator);
vm.expectEmit(false, false, false, true, address(stakeTable));
emit Ownable.OwnershipTransferred(exampleTokenCreator, makeAddr("admin"));
stakeTable.transferOwnership(makeAddr("admin"));
assertEq(stakeTable.owner(), 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(
abi.encodeWithSelector(
Ownable.OwnableUnauthorizedAccount.selector, makeAddr("randomUser")
)
);
stakeTable.transferOwnership(makeAddr("admin"));
vm.stopPrank();

vm.prank(exampleTokenCreator);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0)));
stakeTable.transferOwnership(address(0));
}

// test update min stake amount succeeds
function test_updateMinStakeAmount_succeeds() public {
vm.prank(exampleTokenCreator);
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(
abi.encodeWithSelector(
Ownable.OwnableUnauthorizedAccount.selector, makeAddr("randomUser")
)
);
stakeTable.updateMinStakeAmount(10 ether);
vm.stopPrank();

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

// test update max churn rate succeeds
function test_updateMaxChurnRate_succeeds() public {
vm.prank(exampleTokenCreator);
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(
abi.encodeWithSelector(
Ownable.OwnableUnauthorizedAccount.selector, makeAddr("randomUser")
)
);
stakeTable.updateMaxChurnRate(10);
vm.stopPrank();

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

// test update light client address succeeds
function test_updateLightClientAddress_succeeds() public {
vm.prank(exampleTokenCreator);
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(
abi.encodeWithSelector(
Ownable.OwnableUnauthorizedAccount.selector, makeAddr("randomUser")
)
);
stakeTable.updateLightClientAddress(makeAddr("lightClient"));
vm.stopPrank();

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

0 comments on commit 6014858

Please sign in to comment.