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

Staking rewards #534

Merged
merged 42 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c6c290d
`IERC20Mintable`, not `IERC20`
feuGeneA Sep 6, 2024
318c277
issue ERC20 staking rewards (native rewards TBD)
feuGeneA Sep 6, 2024
f45cce7
mint(addr,amount): respect _MAX_MINT
feuGeneA Sep 10, 2024
57a519e
incorporate uptime in rewards
feuGeneA Sep 11, 2024
f26ecbe
increment validator rewards, not overwrite them
feuGeneA Sep 11, 2024
fd0aa94
Merge branch 'staking-contract' into staking-rewards
geoff-vball Sep 16, 2024
6ba67b6
Fixup
geoff-vball Sep 16, 2024
56304eb
Fix tests
geoff-vball Sep 16, 2024
08d95b8
Fix rewards calculator
geoff-vball Sep 16, 2024
f2c13d5
Fixups
geoff-vball Sep 16, 2024
267d92e
Fixups
geoff-vball Sep 16, 2024
dabde59
Fixups
geoff-vball Sep 16, 2024
d480b4c
Fixups
geoff-vball Sep 16, 2024
2ecf2bf
lint
geoff-vball Sep 16, 2024
e29827c
Native staking rewards
feuGeneA Sep 11, 2024
29259c5
Merge pull request #552 from ava-labs/gstuart/native-staking-rewards
geoff-vball Sep 17, 2024
44d8097
Updates and fixes
geoff-vball Sep 18, 2024
9019085
Remove unneccessary comment
geoff-vball Sep 18, 2024
2109a68
Add comments
geoff-vball Sep 18, 2024
69242fd
Merge branch 'validation-ends-first' into staking-rewards
geoff-vball Sep 18, 2024
8d2969a
Update contracts/staking/PoSValidatorManager.sol
geoff-vball Sep 18, 2024
b465fba
Update contracts/staking/PoSValidatorManager.sol
geoff-vball Sep 18, 2024
c215b3c
Update contracts/staking/PoSValidatorManager.sol
geoff-vball Sep 18, 2024
d9ae361
fix comment
geoff-vball Sep 18, 2024
9bbfde2
review fixes
geoff-vball Sep 18, 2024
7d96776
Merge branch 'validation-ends-first' into staking-rewards
geoff-vball Sep 18, 2024
bda2341
Merge branch 'validation-ends-first' into staking-rewards
geoff-vball Sep 18, 2024
ebe122e
lint
geoff-vball Sep 18, 2024
8a791c1
Merge branch 'staking-contract' into staking-rewards
geoff-vball Sep 18, 2024
13e210b
Review fixes
geoff-vball Sep 18, 2024
061c125
Review fixes
geoff-vball Sep 18, 2024
19e28b3
Fix unit tests
geoff-vball Sep 18, 2024
d5e5e26
Function for withdrawing delegation fees
geoff-vball Sep 18, 2024
c9b1556
Hook up native minter precompile to PoS tests
geoff-vball Sep 18, 2024
bc7ddc7
wait for success in AddNativeMinterAdmin
iansuvak Sep 18, 2024
9e80079
lint
iansuvak Sep 18, 2024
015d92e
Review fixes
geoff-vball Sep 19, 2024
daad525
Check for owner when initializing validator completion
geoff-vball Sep 19, 2024
3286116
Small fix
geoff-vball Sep 19, 2024
6b1e9c0
Small fix
geoff-vball Sep 19, 2024
db44dab
Emit rewards and fees on delegator exit
geoff-vball Sep 19, 2024
099bf13
add test
geoff-vball Sep 19, 2024
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
634 changes: 634 additions & 0 deletions abi-bindings/go/INativeMinter/INativeMinter.go

Large diffs are not rendered by default.

43 changes: 32 additions & 11 deletions abi-bindings/go/mocks/ExampleERC20/ExampleERC20.go

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions contracts/mocks/ExampleERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import {
ERC20Burnable,
ERC20
} from "@openzeppelin/[email protected]/token/ERC20/extensions/ERC20Burnable.sol";
import {IERC20Mintable} from "../staking/interfaces/IERC20Mintable.sol";

contract ExampleERC20 is ERC20Burnable {
contract ExampleERC20 is ERC20Burnable, IERC20Mintable {
string private constant _TOKEN_NAME = "Mock Token";
string private constant _TOKEN_SYMBOL = "EXMP";

uint256 private constant _MAX_MINT = 1e16;
uint256 private constant _MAX_MINT = 1e19;

constructor() ERC20(_TOKEN_NAME, _TOKEN_SYMBOL) {
Copy link
Contributor

Choose a reason for hiding this comment

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

thinking if this example ERC20 should only give access to mint to an ownable address. Even for an example seems like a pretty common check that would exist.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not quite sure what you mean

_mint(msg.sender, 1e28);
Expand All @@ -30,4 +31,11 @@ contract ExampleERC20 is ERC20Burnable {

_mint(msg.sender, amount);
}

function mint(address account, uint256 amount) external {
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
// Can only mint 10 at a time.
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
require(amount <= _MAX_MINT, "ExampleERC20: max mint exceeded");

_mint(account, amount);
}
}
24 changes: 16 additions & 8 deletions contracts/staking/ERC20TokenStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pragma solidity 0.8.25;
import {IERC20TokenStakingManager} from "./interfaces/IERC20TokenStakingManager.sol";
import {Initializable} from
"@openzeppelin/[email protected]/proxy/utils/Initializable.sol";
import {IERC20} from "@openzeppelin/[email protected]/token/ERC20/IERC20.sol";
import {IERC20Mintable} from "./interfaces/IERC20Mintable.sol";
import {SafeERC20TransferFrom} from "@utilities/SafeERC20TransferFrom.sol";
import {SafeERC20} from "@openzeppelin/[email protected]/token/ERC20/utils/SafeERC20.sol";
import {ICMInitializable} from "../utilities/ICMInitializable.sol";
Expand All @@ -21,13 +21,13 @@ contract ERC20TokenStakingManager is
PoSValidatorManager,
IERC20TokenStakingManager
{
using SafeERC20 for IERC20;
using SafeERC20TransferFrom for IERC20;
using SafeERC20 for IERC20Mintable;
using SafeERC20TransferFrom for IERC20Mintable;

// solhint-disable private-vars-leading-underscore
/// @custom:storage-location erc7201:avalanche-icm.storage.ERC20TokenStakingManager
struct ERC20TokenStakingManagerStorage {
IERC20 _token;
IERC20Mintable _token;
uint8 _tokenDecimals;
}
// solhint-enable private-vars-leading-underscore
Expand Down Expand Up @@ -63,22 +63,25 @@ contract ERC20TokenStakingManager is
*/
function initialize(
PoSValidatorManagerSettings calldata settings,
IERC20 token
IERC20Mintable token
) external reinitializer(2) {
__ERC20TokenStakingManager_init(settings, token);
}

// solhint-disable func-name-mixedcase
function __ERC20TokenStakingManager_init(
PoSValidatorManagerSettings calldata settings,
IERC20 token
IERC20Mintable token
) internal onlyInitializing {
__POS_Validator_Manager_init(settings);
__ERC20TokenStakingManager_init_unchained(token);
}

// solhint-disable func-name-mixedcase
function __ERC20TokenStakingManager_init_unchained(IERC20 token) internal onlyInitializing {
function __ERC20TokenStakingManager_init_unchained(IERC20Mintable token)
internal
onlyInitializing
{
ERC20TokenStakingManagerStorage storage $ = _getERC20StakingManagerStorage();
require(address(token) != address(0), "ERC20TokenStakingManager: zero token address");
$._token = token;
Expand Down Expand Up @@ -116,7 +119,12 @@ contract ERC20TokenStakingManager is
return _getERC20StakingManagerStorage()._token.safeTransferFrom(value);
}

function _unlock(uint256 value, address to) internal virtual override {
function _unlock(address to, uint256 value) internal virtual override {
_getERC20StakingManagerStorage()._token.safeTransfer(to, value);
}

function _reward(address account, uint256 amount) internal virtual override {
ERC20TokenStakingManagerStorage storage $ = _getERC20StakingManagerStorage();
$._token.mint(account, amount);
}
}
20 changes: 17 additions & 3 deletions contracts/staking/ExampleRewardCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {IRewardCalculator} from "./interfaces/IRewardCalculator.sol";
contract ExampleRewardCalculator is IRewardCalculator {
uint256 public constant SECONDS_IN_YEAR = 31536000;

uint8 public constant UPTIME_REWARDS_THRESHOLD_PERCENTAGE = 80;

uint64 public immutable rewardBasisPoints;

constructor(uint64 rewardBasisPoints_) {
Expand All @@ -21,11 +23,23 @@ contract ExampleRewardCalculator is IRewardCalculator {
*/
function calculateReward(
uint256 stakeAmount,
uint64 startTime,
uint64 endTime,
uint64 validatorStartTime,
uint64 stakingStartTime,
uint64 stakingEndTime,
uint64 uptimeSeconds,
uint256, // initialSupply
uint256 // endSupply
) external view returns (uint256) {
return (stakeAmount * rewardBasisPoints * (endTime - startTime)) / SECONDS_IN_YEAR / 1000;
// Equivalent to uptimeSeconds/(validator.endedAt - validator.startedAt) < UPTIME_REWARDS_THRESHOLD_PERCENTAGE/100
// Rearranged to prevent integer division truncation.
if (
uptimeSeconds * 100
< (stakingEndTime - validatorStartTime) * UPTIME_REWARDS_THRESHOLD_PERCENTAGE
) {
return 0;
}

return (stakeAmount * rewardBasisPoints * (stakingEndTime - stakingStartTime))
/ SECONDS_IN_YEAR / 10000;
}
}
11 changes: 10 additions & 1 deletion contracts/staking/NativeTokenStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
pragma solidity 0.8.25;

import {INativeTokenStakingManager} from "./interfaces/INativeTokenStakingManager.sol";
import {INativeMinter} from
"@avalabs/[email protected]/contracts/interfaces/INativeMinter.sol";
import {Address} from "@openzeppelin/[email protected]/utils/Address.sol";
import {Initializable} from
"@openzeppelin/[email protected]/proxy/utils/Initializable.sol";
Expand All @@ -21,6 +23,9 @@ contract NativeTokenStakingManager is
{
using Address for address payable;

INativeMinter public constant NATIVE_MINTER =
INativeMinter(0x0200000000000000000000000000000000000001);

constructor(ICMInitializable init) {
if (init == ICMInitializable.Disallowed) {
_disableInitializers();
Expand Down Expand Up @@ -80,7 +85,11 @@ contract NativeTokenStakingManager is
return value;
}

function _unlock(uint256 value, address to) internal virtual override {
function _unlock(address to, uint256 value) internal virtual override {
payable(to).sendValue(value);
}

function _reward(address account, uint256 amount) internal virtual override {
NATIVE_MINTER.mintNativeCoin(account, amount);
}
}
131 changes: 103 additions & 28 deletions contracts/staking/PoSValidatorManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ abstract contract PoSValidatorManager is
IRewardCalculator _rewardCalculator;
/// @notice Maps the validation ID to its requirements.
mapping(bytes32 validationID => PoSValidatorRequirements) _validatorRequirements;
/// @notice Maps the delegationID to the delegator information.
/// @notice Maps the delegation ID to the delegator information.
mapping(bytes32 delegationID => Delegator) _delegatorStakes;
/// @notice Maps the delegation ID to its pending staking rewards.
mapping(bytes32 delegationID => uint256) _redeemableDelegatorRewards;
/// @notice Maps the validation ID to its pending staking rewards.
mapping(bytes32 validationID => uint256) _redeemableValidatorRewards;
/// @notice Saves the uptime of a pending completed or completed validation period so that delegators can collect rewards.
mapping(bytes32 validationID => uint64) _completedValidationUptimeSeconds;
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
}
// solhint-enable private-vars-leading-underscore

Expand Down Expand Up @@ -130,32 +136,83 @@ abstract contract PoSValidatorManager is
$._rewardCalculator = rewardCalculator;
}

function claimDelegationFees(bytes32 validationID) external {
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();

Validator memory validator = getValidator(validationID);

require(
validator.status == ValidatorStatus.Completed,
"PoSValidatorManager: validation period not completed"
);
michaelkaplan13 marked this conversation as resolved.
Show resolved Hide resolved
require(
$._validatorRequirements[validationID].owner == _msgSender(),
"PoSValidatorManager: validator not owned by sender"
);

uint256 rewards = $._redeemableValidatorRewards[validationID];
delete $._redeemableValidatorRewards[validationID];
_reward($._validatorRequirements[validationID].owner, rewards);
}

function initializeEndValidation(
bytes32 validationID,
bool includeUptimeProof,
uint32 messageIndex
) external {
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();
// Check that minimum stake duration has passed
Validator memory validator = getValidator(validationID);

geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
Validator memory validator = _initializeEndValidation(validationID);
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved

if (!_isPoSValidator(validationID)) {
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
return;
}

// Check that minimum stake duration has passed.
require(
block.timestamp
validator.endedAt
>= validator.startedAt + $._validatorRequirements[validationID].minStakeDuration,
"PoSValidatorManager: minimum stake duration not met"
);

if (includeUptimeProof) {
_getUptime(validationID, messageIndex);
// Uptime proofs include the absolute number of seconds the validator has been active.
uint64 uptimeSeconds = _getUptime(validationID, messageIndex);
// Save this value for use by this validator's delegators.
$._completedValidationUptimeSeconds[validationID] = uptimeSeconds;

$._redeemableValidatorRewards[validationID] += $._rewardCalculator.calculateReward({
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
stakeAmount: weightToValue(validator.startingWeight),
validatorStartTime: validator.startedAt,
stakingStartTime: validator.startedAt,
stakingEndTime: validator.endedAt,
uptimeSeconds: uptimeSeconds,
initialSupply: 0,
endSupply: 0
});
}
// TODO: Calculate the reward for the validator, but do not unlock it

_initializeEndValidation(validationID);
}
feuGeneA marked this conversation as resolved.
Show resolved Hide resolved

function completeEndValidation(uint32 messageIndex) external {
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved

(bytes32 validationID, Validator memory validator) = _completeEndValidation(messageIndex);
_unlock(validator.startingWeight, $._validatorRequirements[validationID].owner);

// Return now if this was originally a PoA validator that was later migrated to this PoS manager
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
if (!_isPoSValidator(validationID)) {
return;
}

address owner = $._validatorRequirements[validationID].owner;
// The validator can either be Completed or Invalidated here. We only grant rewards for Completed.
if (validator.status == ValidatorStatus.Completed) {
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
uint256 rewards = $._redeemableValidatorRewards[validationID];
delete $._redeemableValidatorRewards[validationID];
_reward(owner, rewards);
}

// We unlock the stake whether the validation period is completed or invalidated.
_unlock(owner, weightToValue(validator.startingWeight));
}

function _getUptime(bytes32 validationID, uint32 messageIndex) internal view returns (uint64) {
Expand Down Expand Up @@ -226,7 +283,7 @@ abstract contract PoSValidatorManager is
}

function _lock(uint256 value) internal virtual returns (uint256);
function _unlock(uint256 value, address to) internal virtual;
function _unlock(address to, uint256 value) internal virtual;

function _initializeDelegatorRegistration(
bytes32 validationID,
Expand Down Expand Up @@ -331,13 +388,6 @@ abstract contract PoSValidatorManager is
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

Delegator memory delegator = $._delegatorStakes[delegationID];
Validator memory validator = getValidator(validationID);

Expand All @@ -355,19 +405,36 @@ abstract contract PoSValidatorManager is
// initialize the removal.
delegator.status = DelegatorStatus.PendingRemoved;

uint64 validatorUptimeSeconds;
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
if (validator.status == ValidatorStatus.Active) {
if (includeUptimeProof) {
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
// Uptime proofs include the absolute number of seconds the validator has been active.
validatorUptimeSeconds = _getUptime(validationID, messageIndex);
}
uint64 newValidatorWeight = validator.weight - delegator.weight;
(delegator.endingNonce,) = _setValidatorWeight(validationID, newValidatorWeight);

delegator.endedAt = uint64(block.timestamp);
} else {
// If the validation period has already ended, there won't be any uptime message able to be
// provided in the call to initializeEndDelegation, and the uptime submitted when the validator
// was ended should be used to calculate the delegators rewards.
// If the validation period has already ended, we have saved the uptime.
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
// Further, it is impossible to retrieve an uptime proof for an already ended validation,
// so there's no need to check any uptime proof provided in this function call.
validatorUptimeSeconds = $._completedValidationUptimeSeconds[validationID];

delegator.endingNonce = validator.messageNonce;
delegator.endedAt = validator.endedAt;
}

$._redeemableDelegatorRewards[delegationID] = $._rewardCalculator.calculateReward({
stakeAmount: weightToValue(delegator.weight),
validatorStartTime: validator.startedAt,
stakingStartTime: delegator.startedAt,
stakingEndTime: delegator.endedAt,
uptimeSeconds: validatorUptimeSeconds,
initialSupply: 0,
endSupply: 0
});

$._delegatorStakes[delegationID] = delegator;

emit DelegatorRemovalInitialized({
Expand Down Expand Up @@ -416,6 +483,8 @@ abstract contract PoSValidatorManager is
ValidatorMessages.unpackSubnetValidatorWeightUpdateMessage(warpMessage.payload);

Validator memory validator = getValidator(validationID);
Delegator memory delegator = $._delegatorStakes[delegationID];

// The received nonce should be no greater than the highest sent nonce. This should never
// happen since the staking manager is the only entity that can trigger a weight update
// on the P-Chain.
Expand All @@ -425,29 +494,35 @@ abstract contract PoSValidatorManager is
// a weight update using a higher nonce (which implicitly includes the delegation's weight update)
// to be used to complete delisting for an earlier delegation. This is necessary because the P-Chain
// is only willing to sign the latest weight update.
require(
$._delegatorStakes[delegationID].endingNonce <= nonce,
"PoSValidatorManager: nonce does not match"
);
require(delegator.endingNonce <= nonce, "PoSValidatorManager: nonce does not match");

// Ensure the delegator is pending removed. Since anybody can call this function once
// end delegation has been initialized, we need to make sure that this function is only
// callable after that has been done.
require(
$._delegatorStakes[delegationID].status == DelegatorStatus.PendingRemoved,
delegator.status == DelegatorStatus.PendingRemoved,
"PoSValidatorManager: delegation not pending added"
);

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

Choose a reason for hiding this comment

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

I realized we don't actually need a DelegatorStatus.Completed since we can clear the delegator's state when we get to this stage. I can remove it from the enum if others thinks that makes sense


Delegator memory delegator = $._delegatorStakes[delegationID];
_unlock(delegator.weight, delegator.owner);
// TODO: issue rewards
uint256 rewards = $._redeemableDelegatorRewards[delegationID];
delete $._redeemableDelegatorRewards[delegationID];

uint256 validatorFees =
rewards * $._validatorRequirements[validationID].delegationFeeBips / 10000;
$._redeemableValidatorRewards[validationID] += validatorFees;

_reward(delegator.owner, rewards - validatorFees);
_unlock(delegator.owner, weightToValue(delegator.weight));
delete $._delegatorStakes[delegationID];

emit DelegationEnded(delegationID, validationID, nonce);
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
}

function _reward(address account, uint256 amount) internal virtual;

function _isPoSValidator(bytes32 validationID) internal view returns (bool) {
PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();
return $._validatorRequirements[validationID].owner != address(0);
Expand Down
Loading