-
Notifications
You must be signed in to change notification settings - Fork 22
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
Staking rewards #534
Changes from 36 commits
c6c290d
318c277
f45cce7
57a519e
f26ecbe
fd0aa94
6ba67b6
56304eb
08d95b8
f2c13d5
267d92e
dabde59
d480b4c
2ecf2bf
e29827c
29259c5
44d8097
9019085
2109a68
69242fd
8d2969a
b465fba
c215b3c
d9ae361
9bbfde2
7d96776
bda2341
ebe122e
8a791c1
13e210b
061c125
19e28b3
d5e5e26
c9b1556
bc7ddc7
9e80079
015d92e
daad525
3286116
6b1e9c0
db44dab
099bf13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
_mint(msg.sender, 1e28); | ||
|
@@ -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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
@@ -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 | ||
|
@@ -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; | ||
|
@@ -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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
@@ -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(); | ||
|
@@ -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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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) { | ||
|
@@ -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, | ||
|
@@ -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); | ||
|
||
|
@@ -355,19 +405,36 @@ abstract contract PoSValidatorManager is | |
// initialize the removal. | ||
delegator.status = DelegatorStatus.PendingRemoved; | ||
|
||
uint64 validatorUptimeSeconds; | ||
|
||
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({ | ||
|
@@ -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. | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realized we don't actually need a |
||
|
||
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); | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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