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 2 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
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.

7 changes: 6 additions & 1 deletion contracts/mocks/ExampleERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ 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";

Expand All @@ -30,4 +31,8 @@ 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
_mint(account, amount);
}
}
22 changes: 15 additions & 7 deletions contracts/staking/ERC20TokenStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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 @@ -20,13 +20,13 @@
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 @@ -62,26 +62,29 @@
*/
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;
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning


/**
* @notice Begins the validator registration process. Locks the configured ERC20 in the contract as the stake.
Expand Down Expand Up @@ -120,4 +123,9 @@
function _unlock(uint256 value, address to) internal virtual override {
_getERC20StakingManagerStorage()._token.safeTransfer(to, value);
}

function _reward(address account, uint256 amount) internal virtual override {
ERC20TokenStakingManagerStorage storage $ = _getERC20StakingManagerStorage();
$._token.mint(account, amount);
}
}
5 changes: 5 additions & 0 deletions contracts/staking/NativeTokenStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,9 @@ contract NativeTokenStakingManager is
function _unlock(uint256 value, address to) internal virtual override {
payable(to).sendValue(value);
}

// solhint-disable-next-line no-empty-blocks
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
function _reward(address account, uint256 amount) internal virtual override {
// TODO: call the native minter precompile to mint `amount` for `account`
}
}
16 changes: 15 additions & 1 deletion contracts/staking/PoSValidatorManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ abstract contract PoSValidatorManager is IPoSValidatorManager, ValidatorManager

function completeEndValidation(uint32 messageIndex) external {
Validator memory validator = _completeEndValidation(messageIndex);

PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage();
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
uint256 rewardAmount = $._rewardCalculator.calculateReward(
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
validator.weight, validator.startedAt, validator.endedAt, 0, 0
);

_reward(validator.owner, rewardAmount);

_unlock(validator.startingWeight, validator.owner);
}

Expand Down Expand Up @@ -356,7 +364,11 @@ abstract contract PoSValidatorManager is IPoSValidatorManager, ValidatorManager

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

uint256 rewardAmount = $._rewardCalculator.calculateReward(
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
delegator.weight, delegator.startedAt, delegator.endedAt, 0, 0
);
_reward(delegator.owner, rewardAmount);

emit DelegationEnded(delegationID, validationID, nonce);
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
}
Expand All @@ -378,4 +390,6 @@ abstract contract PoSValidatorManager is IPoSValidatorManager, ValidatorManager
"PoSValidatorManager: delegation registration not pending"
);
}

function _reward(address account, uint256 amount) internal virtual;
}
12 changes: 12 additions & 0 deletions contracts/staking/interfaces/IERC20Mintable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// (c) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// SPDX-License-Identifier: Ecosystem

pragma solidity 0.8.25;

import {IERC20} from "@openzeppelin/[email protected]/token/ERC20/IERC20.sol";

interface IERC20Mintable is IERC20 {
function mint(address account, uint256 amount) external;
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved
}
15 changes: 11 additions & 4 deletions contracts/staking/tests/ERC20TokenStakingManagerTests.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,27 @@ import {PoSValidatorManagerTest} from "./PoSValidatorManagerTests.t.sol";
import {ERC20TokenStakingManager} from "../ERC20TokenStakingManager.sol";
import {ValidatorManagerSettings} from "../interfaces/IValidatorManager.sol";
import {PoSValidatorManagerSettings} from "../interfaces/IPoSValidatorManager.sol";
import {IRewardCalculator} from "../interfaces/IRewardCalculator.sol";
import {ExampleRewardCalculator} from "../ExampleRewardCalculator.sol";
import {ICMInitializable} from "../../utilities/ICMInitializable.sol";
import {ExampleERC20} from "@mocks/ExampleERC20.sol";
import {IERC20} from "@openzeppelin/[email protected]/token/ERC20/IERC20.sol";
import {IERC20Mintable} from "../interfaces/IERC20Mintable.sol";
import {SafeERC20} from "@openzeppelin/[email protected]/token/ERC20/utils/SafeERC20.sol";

// TODO: Remove this once all unit tests implemented
// solhint-disable no-empty-blocks
contract ERC20TokenStakingManagerTest is PoSValidatorManagerTest {
using SafeERC20 for IERC20;
using SafeERC20 for IERC20Mintable;

ERC20TokenStakingManager public app;
IERC20 public token;
IERC20Mintable public token;
ExampleRewardCalculator public rewardCalculator;

function setUp() public virtual {
// Construct the object under test
app = new ERC20TokenStakingManager(ICMInitializable.Allowed);
token = new ExampleERC20();
rewardCalculator = new ExampleRewardCalculator(DEFAULT_REWARD_RATE);
app.initialize(
PoSValidatorManagerSettings({
baseSettings: ValidatorManagerSettings({
Expand All @@ -37,7 +40,7 @@ contract ERC20TokenStakingManagerTest is PoSValidatorManagerTest {
minimumStakeAmount: DEFAULT_MINIMUM_STAKE,
maximumStakeAmount: DEFAULT_MAXIMUM_STAKE,
minimumStakeDuration: DEFAULT_MINIMUM_STAKE_DURATION,
rewardCalculator: IRewardCalculator(address(0))
rewardCalculator: rewardCalculator
}),
token
);
Expand Down Expand Up @@ -83,6 +86,10 @@ contract ERC20TokenStakingManagerTest is PoSValidatorManagerTest {
vm.expectCall(address(token), abi.encodeCall(IERC20.transfer, (account, amount)));
}

function _expectRewardIssuance(address account, uint256 amount) internal override {
vm.expectCall(address(token), abi.encodeCall(IERC20Mintable.mint, (account, amount)));
}

function _getStakeAssetBalance(address account) internal view override returns (uint256) {
return token.balanceOf(account);
}
Expand Down
17 changes: 16 additions & 1 deletion contracts/staking/tests/NativeTokenStakingManagerTests.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {NativeTokenStakingManager} from "../NativeTokenStakingManager.sol";
import {ValidatorManagerSettings} from "../interfaces/IValidatorManager.sol";
import {PoSValidatorManagerSettings} from "../interfaces/IPoSValidatorManager.sol";
import {IRewardCalculator} from "../interfaces/IRewardCalculator.sol";
import {ExampleRewardCalculator} from "../ExampleRewardCalculator.sol";
import {ICMInitializable} from "../../utilities/ICMInitializable.sol";

// TODO: Remove this once all unit tests implemented
Expand All @@ -30,13 +31,25 @@ contract NativeTokenStakingManagerTest is PoSValidatorManagerTest {
minimumStakeAmount: DEFAULT_MINIMUM_STAKE,
maximumStakeAmount: DEFAULT_MAXIMUM_STAKE,
minimumStakeDuration: DEFAULT_MINIMUM_STAKE_DURATION,
rewardCalculator: IRewardCalculator(address(0))
rewardCalculator: IRewardCalculator(new ExampleRewardCalculator(DEFAULT_REWARD_RATE))
})
);
validatorManager = app;
posValidatorManager = app;
}

function testCompleteEndValidation() public override {
// TODO: get native token staking rewards working, then remove this
// method and let the implementation in PosValidatorManagerTests do the
// test, and remove the `virtual` modifier from that implementation.
}

function testCompleteEndDelegation() public override {
// TODO: get native token staking rewards working, then remove this
// method and let the implementation in PosValidatorManagerTests do the
// test, and remove the `virtual` modifier from that implementation.
}

// Helpers
function _initializeValidatorRegistration(
bytes32 nodeID,
Expand Down Expand Up @@ -69,6 +82,8 @@ contract NativeTokenStakingManagerTest is PoSValidatorManagerTest {
vm.expectCall(account, amount, "");
}

function _expectRewardIssuance(address account, uint256 amount) internal override {}

function _getStakeAssetBalance(address account) internal view override returns (uint256) {
return account.balance;
}
Expand Down
52 changes: 35 additions & 17 deletions contracts/staking/tests/PoSValidatorManagerTests.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest {
uint64 public constant DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP = uint64(4000);
address public constant DEFAULT_DELEGATOR_ADDRESS =
address(0x1234123412341234123412341234123412341234);
uint64 public constant DEFAULT_REWARD_RATE = uint64(10);

PoSValidatorManager public posValidatorManager;

Expand Down Expand Up @@ -371,14 +372,18 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest {
posValidatorManager.resendEndDelegation(delegationID);
}

function testCompleteEndDelegation() public {
function testCompleteEndDelegation() public virtual {
uint256 registrationDuration = 1000 * 60 * 60 * 24; // 1 day

uint256 registrationExpiry = vm.getBlockTimestamp() + registrationDuration;

bytes32 validationID = _setUpCompleteValidatorRegistration({
nodeID: DEFAULT_NODE_ID,
subnetID: DEFAULT_SUBNET_ID,
weight: DEFAULT_WEIGHT,
registrationExpiry: DEFAULT_EXPIRY,
registrationExpiry: uint64(registrationExpiry),
blsPublicKey: DEFAULT_BLS_PUBLIC_KEY,
registrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP
registrationTimestamp: uint64(registrationExpiry)
});
bytes32 delegationID = _setUpInitializeDelegatorRegistration({
validationID: validationID,
Expand All @@ -388,32 +393,34 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest {
expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT,
expectedNonce: 1
});
_setUpCompleteDelegatorRegistration(
validationID,
delegationID,
DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP,
DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT,
1
);
_setUpCompleteDelegatorRegistration({
validationID: validationID,
delegationID: delegationID,
completeRegistrationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP,
expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT,
expectedNonce: 1
});
_setUpInitializeEndDelegation({
validationID: validationID,
delegatorAddress: DEFAULT_DELEGATOR_ADDRESS,
delegationID: delegationID,
endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP,
endDelegationTimestamp: uint64(registrationExpiry),
expectedValidatorWeight: DEFAULT_WEIGHT,
expectedNonce: 2
});

address delegator = DEFAULT_DELEGATOR_ADDRESS;
uint256 balanceBefore = _getStakeAssetBalance(delegator);
uint256 expectedReward = DEFAULT_DELEGATOR_WEIGHT * DEFAULT_REWARD_RATE / 365;
_expectStakeUnlock(delegator, DEFAULT_DELEGATOR_WEIGHT);
_expectRewardIssuance(delegator, expectedReward);

_setUpCompleteEndDelegation(validationID, delegationID, DEFAULT_WEIGHT, DEFAULT_WEIGHT, 2);

uint256 balanceChange = _getStakeAssetBalance(delegator) - balanceBefore;
require(
balanceChange >= DEFAULT_DELEGATOR_WEIGHT,
"delegator should have received their stake back"
balanceChange == DEFAULT_DELEGATOR_WEIGHT + expectedReward,
"delegator should have received their stake back and been rewarded"
);
}

Expand Down Expand Up @@ -557,25 +564,35 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest {
_setUpCompleteEndDelegation(validationID, delegationID1, DEFAULT_WEIGHT, DEFAULT_WEIGHT, 4);
}

function testCompleteEndValidation() public override {
function testCompleteEndValidation() public virtual override {
uint256 registrationDuration = 1000 * 60 * 60 * 24; // 1 day

uint256 registrationExpiry = vm.getBlockTimestamp() + registrationDuration;

bytes32 validationID = _setUpInitializeEndValidation({
nodeID: DEFAULT_NODE_ID,
subnetID: DEFAULT_SUBNET_ID,
weight: DEFAULT_WEIGHT,
registrationExpiry: DEFAULT_EXPIRY,
registrationExpiry: uint64(registrationExpiry),
blsPublicKey: DEFAULT_BLS_PUBLIC_KEY,
registrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP,
completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP
completionTimestamp: uint64(registrationExpiry)
});

uint256 balanceBefore = _getStakeAssetBalance(address(this));

uint256 expectedReward = DEFAULT_WEIGHT * DEFAULT_REWARD_RATE / 365 - 1;
geoff-vball marked this conversation as resolved.
Show resolved Hide resolved

_expectStakeUnlock(address(this), DEFAULT_WEIGHT);
_expectRewardIssuance(address(this), expectedReward);

_testCompleteEndValidation(validationID);

uint256 balanceChange = _getStakeAssetBalance(address(this)) - balanceBefore;
require(balanceChange == DEFAULT_WEIGHT, "validator should have received their stake back");
require(
balanceChange == DEFAULT_WEIGHT + expectedReward,
"validator should have received their stake back and been rewarded"
);
}

function testValueToWeight() public view {
Expand Down Expand Up @@ -713,6 +730,7 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest {

function _getStakeAssetBalance(address account) internal virtual returns (uint256);
function _expectStakeUnlock(address account, uint256 amount) internal virtual;
function _expectRewardIssuance(address account, uint256 amount) internal virtual;

function _formatErrorMessage(bytes memory errorMessage) internal pure returns (bytes memory) {
return abi.encodePacked("PoSValidatorManager: ", errorMessage);
Expand Down
2 changes: 1 addition & 1 deletion scripts/abi_bindings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export ARCH=$(uname -m)
[ $ARCH = x86_64 ] && ARCH=amd64
echo "ARCH set to $ARCH"

DEFAULT_CONTRACT_LIST="TeleporterMessenger TeleporterRegistry ExampleERC20 TestMessenger ValidatorSetSig NativeTokenStakingManager ERC20TokenStakingManager PoAValidatorManager"
DEFAULT_CONTRACT_LIST="TeleporterMessenger TeleporterRegistry ExampleERC20 ExampleRewardCalculator TestMessenger ValidatorSetSig NativeTokenStakingManager ERC20TokenStakingManager PoAValidatorManager"

PROXY_LIST="TransparentUpgradeableProxy ProxyAdmin"

Expand Down
Loading