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

WIP: Staking contract #453

Closed
wants to merge 501 commits into from
Closed

WIP: Staking contract #453

wants to merge 501 commits into from

Conversation

geoff-vball
Copy link
Contributor

This is a draft PR for the Staking Contract feature branch. This is to make sure CI is passing, and to have a place for team members to leave comments.

This should roughly track with this milestone https://github.com/ava-labs/teleporter/milestone/2

contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
Comment on lines 38 to 40
function _unlock(uint256 value, address to) internal virtual override {
payable(to).sendValue(value);
}

Check warning

Code scanning / Slither

Dead-code

NativeTokenStakingManager._unlock(uint256,address) (contracts/staking/NativeTokenStakingManager.sol#38-40) is never used and should be removed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
contracts/staking/StakingManager.sol Fixed Show fixed Hide fixed
// Ensure the registration expiry is in a valid range.
require(
registrationExpiry > block.timestamp && block.timestamp + 2 days > registrationExpiry,
"StakingManager: Invalid registration expiry"
Copy link

Choose a reason for hiding this comment

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

Solidity 0.8.26 allows you to use custom errors in require(). They result in smaller contract sizes and are IMO much better for tests because you can assert the exact reason for a failure without risk of introducing a change-detector test if you modify the error string. If you want to stick to 0.8.25 then I'd recommend switching to if (!cond) { revert SpecificError() } where you originally had require(cond, ...).

Copy link
Contributor

Choose a reason for hiding this comment

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

Our versioning policy is to use the second-most-recent release to hedge against potential issues in the latest release. That said, we've struggled to choose between revert strings and custom errors in the past, so getting the best of both worlds will be a welcome change.

// (c) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// SPDX-License-Identifier: Ecosystem
Copy link

Choose a reason for hiding this comment

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

(nit) Confirm with a lawyer, but I think this should be UNLICENSED per Solidity conventions for non-OSS licenses (not UNLICENSE without the D). There are specific license identifiers for SPDX.

* | 52 bytes |
* +----------+
*/
function packSetSubnetValidatorWeightMessage(
Copy link

Choose a reason for hiding this comment

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

This can be simplified to:

return abi.encodePacked(
    SET_SUBNET_VALIDATOR_WEIGHT_MESSAGE_TYPE_ID,
    validationID,
    nonce,
    weight
);

Copy link

Choose a reason for hiding this comment

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

There are similar improvements that can be made to unpacking, but it was easier to write code so I created a PR, based off this branch, to demonstrate.

Together they save about 39,500 gas on the pack-unpack round trip.

Copy link

Choose a reason for hiding this comment

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

I only just saw the TODO re gas efficiency 🤦 sorry!

*/
function initializeEndValidation(
bytes32 validationID,
bool includeUptimeProof,
Copy link

Choose a reason for hiding this comment

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

hmm i wonder if this can lead to a fat-fingering situation by providing the warp message but not enabling this flag. sorry if I'm being too nitpicky.

require(valid, "StakingManager: Invalid warp message");

bytes32 validationID;
if (setWeightMessageType) {
Copy link

Choose a reason for hiding this comment

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

did we confirm that both SetWeightTx=0 and InvalidMsg can be signed for this one? I wonder if it makes a better UX to expect a single msg type to be signed/relayed.

Comment on lines 65 to 71
function __ERC20TokenStakingManager_init(
PoSValidatorManagerSettings calldata settings,
IERC20 token
) internal onlyInitializing {
__POS_Validator_Manager_init(settings);
__ERC20TokenStakingManager_init_unchained(token);
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions

Function ERC20TokenStakingManager.__ERC20TokenStakingManager_init(PoSValidatorManagerSettings,IERC20) (contracts/staking/ERC20TokenStakingManager.sol#65-71) is not in mixedCase
Comment on lines 35 to 40
function __NativeTokenStakingManager_init(PoSValidatorManagerSettings calldata settings)
internal
onlyInitializing
{
__POS_Validator_Manager_init(settings);
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions

Function NativeTokenStakingManager.__NativeTokenStakingManager_init(PoSValidatorManagerSettings) (contracts/staking/NativeTokenStakingManager.sol#35-40) is not in mixedCase
Comment on lines 30 to 36
function __PoAValidatorManager_init(
ValidatorManagerSettings calldata settings,
address initialOwner
) internal onlyInitializing {
__ValidatorManager_init(settings);
__Ownable_init(initialOwner);
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions

Function PoAValidatorManager.__PoAValidatorManager_init(ValidatorManagerSettings,address) (contracts/staking/PoAValidatorManager.sol#30-36) is not in mixedCase
}

// solhint-disable-next-line no-empty-blocks
function __PoAValidatorManager_init_unchained() internal onlyInitializing {}

Check warning

Code scanning / Slither

Dead-code

PoAValidatorManager.__PoAValidatorManager_init_unchained() (contracts/staking/PoAValidatorManager.sol#39) is never used and should be removed
}

// solhint-disable-next-line no-empty-blocks
function __PoAValidatorManager_init_unchained() internal onlyInitializing {}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions

Function PoAValidatorManager.__PoAValidatorManager_init_unchained() (contracts/staking/PoAValidatorManager.sol#39) is not in mixedCase
Comment on lines 157 to 167
function resendRegisterValidatorMessage(bytes32 validationID) external {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();
require(
$._pendingRegisterValidationMessages[validationID].length > 0
&& $._validationPeriods[validationID].status == ValidatorStatus.PendingAdded,
"ValidatorManager: invalid validation ID"
);

// Submit the message to the Warp precompile.
WARP_MESSENGER.sendWarpMessage($._pendingRegisterValidationMessages[validationID]);
}

Check warning

Code scanning / Slither

Unused return

ValidatorManager.resendRegisterValidatorMessage(bytes32) (contracts/staking/ValidatorManager.sol#157-167) ignores return value by WARP_MESSENGER.sendWarpMessage($._pendingRegisterValidationMessages[validationID]) (contracts/staking/ValidatorManager.sol#166)
Comment on lines 213 to 245
function _initializeEndValidation(bytes32 validationID) internal virtual {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();

// Ensure the validation period is active.
Validator memory validator = $._validationPeriods[validationID];
require(
validator.status == ValidatorStatus.Active, "ValidatorManager: validator not active"
);
require(_msgSender() == validator.owner, "ValidatorManager: sender not validator owner");

// Check that removing this validator would not exceed the maximum churn rate.
_checkAndUpdateChurnTracker(validator.weight);

// Update the validator status to pending removal.
// They are not removed from the active validators mapping until the P-Chain acknowledges the removal.
validator.status = ValidatorStatus.PendingRemoved;

// Set the end time of the validation period, since it is no longer known to be an active validator
// on the P-Chain.
validator.endedAt = uint64(block.timestamp);

// Save the validator updates.
// TODO: Optimize storage writes here (probably don't need to write the whole value).
$._validationPeriods[validationID] = validator;

// Submit the message to the Warp precompile.
bytes memory setValidatorWeightPayload = ValidatorMessages
.packSetSubnetValidatorWeightMessage(validationID, validator.messageNonce, 0);
bytes32 messageID = WARP_MESSENGER.sendWarpMessage(setValidatorWeightPayload);

// Emit the event to signal the start of the validator removal process.
emit ValidatorRemovalInitialized(validationID, messageID, validator.weight, block.timestamp);
}

Check notice

Code scanning / Slither

Reentrancy vulnerabilities

Reentrancy in ValidatorManager._initializeEndValidation(bytes32) (contracts/staking/ValidatorManager.sol#213-245): External calls: - messageID = WARP_MESSENGER.sendWarpMessage(setValidatorWeightPayload) (contracts/staking/ValidatorManager.sol#241) Event emitted after the call(s): - ValidatorRemovalInitialized(validationID,messageID,validator.weight,block.timestamp) (contracts/staking/ValidatorManager.sol#244)
Comment on lines 213 to 245
function _initializeEndValidation(bytes32 validationID) internal virtual {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();

// Ensure the validation period is active.
Validator memory validator = $._validationPeriods[validationID];
require(
validator.status == ValidatorStatus.Active, "ValidatorManager: validator not active"
);
require(_msgSender() == validator.owner, "ValidatorManager: sender not validator owner");

// Check that removing this validator would not exceed the maximum churn rate.
_checkAndUpdateChurnTracker(validator.weight);

// Update the validator status to pending removal.
// They are not removed from the active validators mapping until the P-Chain acknowledges the removal.
validator.status = ValidatorStatus.PendingRemoved;

// Set the end time of the validation period, since it is no longer known to be an active validator
// on the P-Chain.
validator.endedAt = uint64(block.timestamp);

// Save the validator updates.
// TODO: Optimize storage writes here (probably don't need to write the whole value).
$._validationPeriods[validationID] = validator;

// Submit the message to the Warp precompile.
bytes memory setValidatorWeightPayload = ValidatorMessages
.packSetSubnetValidatorWeightMessage(validationID, validator.messageNonce, 0);
bytes32 messageID = WARP_MESSENGER.sendWarpMessage(setValidatorWeightPayload);

// Emit the event to signal the start of the validator removal process.
emit ValidatorRemovalInitialized(validationID, messageID, validator.weight, block.timestamp);
}

Check notice

Code scanning / Slither

Block timestamp

ValidatorManager._initializeEndValidation(bytes32) (contracts/staking/ValidatorManager.sol#213-245) uses timestamp for comparisons Dangerous comparisons: - require(bool,string)(validator.status == ValidatorStatus.Active,ValidatorManager: validator not active) (contracts/staking/ValidatorManager.sol#218-220) - require(bool,string)(_msgSender() == validator.owner,ValidatorManager: sender not validator owner) (contracts/staking/ValidatorManager.sol#221)
Comment on lines 252 to 264
function resendEndValidatorMessage(bytes32 validationID) external {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();
Validator memory validator = $._validationPeriods[validationID];

require(
validator.status == ValidatorStatus.PendingRemoved,
"ValidatorManager: validator not pending removal"
);

bytes memory setValidatorWeightPayload = ValidatorMessages
.packSetSubnetValidatorWeightMessage(validationID, validator.messageNonce, 0);
WARP_MESSENGER.sendWarpMessage(setValidatorWeightPayload);
}

Check warning

Code scanning / Slither

Unused return

ValidatorManager.resendEndValidatorMessage(bytes32) (contracts/staking/ValidatorManager.sol#252-264) ignores return value by WARP_MESSENGER.sendWarpMessage(setValidatorWeightPayload) (contracts/staking/ValidatorManager.sol#263)
contracts/staking/ValidatorManager.sol Fixed Show fixed Hide fixed
contracts/staking/PoSValidatorManager.sol Fixed Show fixed Hide fixed
Comment on lines 61 to 70
function _getValidatorManagerStorage()
private
pure
returns (ValidatorManagerStorage storage $)
{
// solhint-disable-next-line no-inline-assembly
assembly {
$.slot := _VALIDATOR_MANAGER_STORAGE_LOCATION
}
}

Check warning

Code scanning / Slither

Assembly usage

ValidatorManager._getValidatorManagerStorage() (contracts/staking/ValidatorManager.sol#61-70) uses assembly - INLINE ASM (contracts/staking/ValidatorManager.sol#67-69)
Comment on lines 329 to 350
function _checkAndUpdateChurnTracker(uint64 amount) internal {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();
if ($._maximumHourlyChurn == 0) {
return;
}

ValidatorChurnPeriod memory churnTracker = $._churnTracker;
uint256 currentTime = block.timestamp;
if (currentTime - churnTracker.startedAt >= 1 hours) {
churnTracker.churnAmount = amount;
churnTracker.startedAt = currentTime;
} else {
churnTracker.churnAmount += amount;
}

uint8 churnPercentage = uint8((churnTracker.churnAmount * 100) / churnTracker.initialStake);
require(
churnPercentage <= $._maximumHourlyChurn,
"ValidatorManager: maximum hourly churn rate exceeded"
);
$._churnTracker = churnTracker;
}

Check notice

Code scanning / Slither

Block timestamp

ValidatorManager._checkAndUpdateChurnTracker(uint64) (contracts/staking/ValidatorManager.sol#329-350) uses timestamp for comparisons Dangerous comparisons: - currentTime - churnTracker.startedAt >= 3600 (contracts/staking/ValidatorManager.sol#337) - require(bool,string)(churnPercentage <= $._maximumHourlyChurn,ValidatorManager: maximum hourly churn rate exceeded) (contracts/staking/ValidatorManager.sol#345-348)
Comment on lines 200 to 205
function resendDelegatorRegistration(bytes32 delegationID) external {
_checkPendingRegisterDelegatorMessages(delegationID);
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();
// Submit the message to the Warp precompile.
WARP_MESSENGER.sendWarpMessage($._pendingRegisterDelegatorMessages[delegationID]);
}

Check warning

Code scanning / Slither

Unused return

PoSValidatorManager.resendDelegatorRegistration(bytes32) (contracts/staking/PoSValidatorManager.sol#200-205) ignores return value by WARP_MESSENGER.sendWarpMessage($._pendingRegisterDelegatorMessages[delegationID]) (contracts/staking/PoSValidatorManager.sol#204)
Comment on lines 207 to 240
function completeDelegatorRegistration(uint32 messageIndex, bytes32 delegationID) external {
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();

// Unpack the Warp message
WarpMessage memory warpMessage = _getPChainWarpMessage(messageIndex);
(bytes32 validationID, uint64 nonce,) =
ValidatorMessages.unpackSubnetValidatorWeightUpdateMessage(warpMessage.payload);
_checkPendingRegisterDelegatorMessages(delegationID);
delete $._pendingRegisterDelegatorMessages[delegationID];

Validator memory validator = _getValidator(validationID);

// The received nonce should be no greater than the highest sent nonce
require(validator.messageNonce >= nonce, "PoSValidatorManager: invalid nonce");
// It should also be greater than or equal to the delegationID's starting nonce
require(
$._delegatorStakes[delegationID].startingNonce <= nonce,
"PoSValidatorManager: nonce does not match"
);

require(
$._delegatorStakes[delegationID].status == DelegatorStatus.PendingAdded,
"PoSValidatorManager: delegationID not pending added"
);
// Update the delegation status
$._delegatorStakes[delegationID].status = DelegatorStatus.Active;
$._delegatorStakes[delegationID].startedAt = uint64(block.timestamp);
emit DelegatorRegistered({
delegationID: delegationID,
validationID: validationID,
nonce: nonce,
startTime: block.timestamp
});
}

Check warning

Code scanning / Slither

Unused return

PoSValidatorManager.completeDelegatorRegistration(uint32,bytes32) (contracts/staking/PoSValidatorManager.sol#207-240) ignores return value by (validationID,nonce,None) = ValidatorMessages.unpackSubnetValidatorWeightUpdateMessage(warpMessage.payload) (contracts/staking/PoSValidatorManager.sol#212-213)
Comment on lines 242 to 291
function initializeEndDelegation(
bytes32 delegationID,
bool includeUptimeProof,
uint32 messageIndex
) external {
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();
bytes32 validationID = $._delegatorStakes[delegationID].validationID;

uint64 uptime;
if (includeUptimeProof) {
uptime = _getUptime(validationID, messageIndex);
}

// TODO: Calculate the delegator's reward, but do not unlock it

// Ensure the delegator is active
Delegator memory delegator = $._delegatorStakes[delegationID];
require(
delegator.owner == _msgSender(), "PoSValidatorManager: delegation not owned by sender"
);
require(
delegator.status == DelegatorStatus.Active, "PoSValidatorManager: delegation not active"
);
uint64 nonce = _getAndIncrementNonce(validationID);
delegator.status = DelegatorStatus.PendingRemoved;
delegator.endedAt = uint64(block.timestamp);
delegator.endingNonce = nonce;

$._delegatorStakes[delegationID] = delegator;

Validator memory validator = _getValidator(validationID);
require(validator.weight > delegator.weight, "PoSValidatorManager: Invalid weight");
uint64 newValidatorWeight = validator.weight - delegator.weight;
_setValidatorWeight(validationID, newValidatorWeight);

// Submit the message to the Warp precompile.
bytes memory setValidatorWeightPayload = ValidatorMessages
.packSetSubnetValidatorWeightMessage(validationID, nonce, newValidatorWeight);
$._pendingEndDelegatorMessages[delegationID] = setValidatorWeightPayload;
bytes32 messageID = WARP_MESSENGER.sendWarpMessage(setValidatorWeightPayload);

emit DelegatorRemovalInitialized({
delegationID: delegationID,
validationID: validationID,
nonce: nonce,
validatorWeight: newValidatorWeight,
endTime: block.timestamp,
setWeightMessageID: messageID
});
}

Check notice

Code scanning / Slither

Reentrancy vulnerabilities

Reentrancy in PoSValidatorManager.initializeEndDelegation(bytes32,bool,uint32) (contracts/staking/PoSValidatorManager.sol#242-291): External calls: - messageID = WARP_MESSENGER.sendWarpMessage(setValidatorWeightPayload) (contracts/staking/PoSValidatorManager.sol#281) Event emitted after the call(s): - DelegatorRemovalInitialized({delegationID:delegationID,validationID:validationID,nonce:nonce,validatorWeight:newValidatorWeight,endTime:block.timestamp,setWeightMessageID:messageID}) (contracts/staking/PoSValidatorManager.sol#283-290)
Comment on lines 242 to 291
function initializeEndDelegation(
bytes32 delegationID,
bool includeUptimeProof,
uint32 messageIndex
) external {
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();
bytes32 validationID = $._delegatorStakes[delegationID].validationID;

uint64 uptime;
if (includeUptimeProof) {
uptime = _getUptime(validationID, messageIndex);
}

// TODO: Calculate the delegator's reward, but do not unlock it

// Ensure the delegator is active
Delegator memory delegator = $._delegatorStakes[delegationID];
require(
delegator.owner == _msgSender(), "PoSValidatorManager: delegation not owned by sender"
);
require(
delegator.status == DelegatorStatus.Active, "PoSValidatorManager: delegation not active"
);
uint64 nonce = _getAndIncrementNonce(validationID);
delegator.status = DelegatorStatus.PendingRemoved;
delegator.endedAt = uint64(block.timestamp);
delegator.endingNonce = nonce;

$._delegatorStakes[delegationID] = delegator;

Validator memory validator = _getValidator(validationID);
require(validator.weight > delegator.weight, "PoSValidatorManager: Invalid weight");
uint64 newValidatorWeight = validator.weight - delegator.weight;
_setValidatorWeight(validationID, newValidatorWeight);

// Submit the message to the Warp precompile.
bytes memory setValidatorWeightPayload = ValidatorMessages
.packSetSubnetValidatorWeightMessage(validationID, nonce, newValidatorWeight);
$._pendingEndDelegatorMessages[delegationID] = setValidatorWeightPayload;
bytes32 messageID = WARP_MESSENGER.sendWarpMessage(setValidatorWeightPayload);

emit DelegatorRemovalInitialized({
delegationID: delegationID,
validationID: validationID,
nonce: nonce,
validatorWeight: newValidatorWeight,
endTime: block.timestamp,
setWeightMessageID: messageID
});
}

Check notice

Code scanning / Slither

Block timestamp

PoSValidatorManager.initializeEndDelegation(bytes32,bool,uint32) (contracts/staking/PoSValidatorManager.sol#242-291) uses timestamp for comparisons Dangerous comparisons: - require(bool,string)(delegator.owner == _msgSender(),PoSValidatorManager: delegation not owned by sender) (contracts/staking/PoSValidatorManager.sol#259-261) - require(bool,string)(delegator.status == DelegatorStatus.Active,PoSValidatorManager: delegation not active) (contracts/staking/PoSValidatorManager.sol#262-264) - require(bool,string)(validator.weight > delegator.weight,PoSValidatorManager: Invalid weight) (contracts/staking/PoSValidatorManager.sol#273)
Comment on lines 293 to 297
function resendEndDelegation(bytes32 delegationID) external {
_checkPendingEndDelegatorMessage(delegationID);
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();
WARP_MESSENGER.sendWarpMessage($._pendingEndDelegatorMessages[delegationID]);
}

Check warning

Code scanning / Slither

Unused return

PoSValidatorManager.resendEndDelegation(bytes32) (contracts/staking/PoSValidatorManager.sol#293-297) ignores return value by WARP_MESSENGER.sendWarpMessage($._pendingEndDelegatorMessages[delegationID]) (contracts/staking/PoSValidatorManager.sol#296)
Comment on lines 299 to 329
function completeEndDelegation(uint32 messageIndex, bytes32 delegationID) external {
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();

// Unpack the Warp message
WarpMessage memory warpMessage = _getPChainWarpMessage(messageIndex);
(bytes32 validationID, uint64 nonce,) =
ValidatorMessages.unpackSubnetValidatorWeightUpdateMessage(warpMessage.payload);
_checkPendingEndDelegatorMessage(delegationID);
delete $._pendingEndDelegatorMessages[delegationID];

Validator memory validator = _getValidator(validationID);
// The received nonce should be no greater than the highest sent nonce
require(validator.messageNonce >= nonce, "PoSValidatorManager: invalid nonce");
// It should also be greater than or equal to the delegator's ending nonce
require(
$._delegatorStakes[delegationID].endingNonce <= nonce,
"PoSValidatorManager: nonce does not match"
);

require(
$._delegatorStakes[delegationID].status == DelegatorStatus.PendingRemoved,
"PoSValidatorManager: delegation not pending added"
);

// Update the delegator status
$._delegatorStakes[delegationID].status = DelegatorStatus.Completed;

// TODO: Unlock the delegator's stake and their reward

emit DelegationEnded(delegationID, validationID, nonce);
}

Check warning

Code scanning / Slither

Unused return

PoSValidatorManager.completeEndDelegation(uint32,bytes32) (contracts/staking/PoSValidatorManager.sol#299-329) ignores return value by (validationID,nonce,None) = ValidatorMessages.unpackSubnetValidatorWeightUpdateMessage(warpMessage.payload) (contracts/staking/PoSValidatorManager.sol#304-305)
@michaelkaplan13 michaelkaplan13 deleted the staking-contract branch September 20, 2024 21:40
@cam-schultz cam-schultz restored the staking-contract branch September 24, 2024 14:30
@cam-schultz cam-schultz reopened this Sep 24, 2024
@michaelkaplan13 michaelkaplan13 deleted the staking-contract branch October 29, 2024 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

8 participants