Skip to content

Commit

Permalink
issue ERC20 staking rewards (native rewards TBD)
Browse files Browse the repository at this point in the history
  • Loading branch information
feuGeneA committed Sep 10, 2024
1 parent c6c290d commit 318c277
Show file tree
Hide file tree
Showing 13 changed files with 435 additions and 31 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions contracts/staking/ERC20TokenStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,9 @@ contract ERC20TokenStakingManager is
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
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();
uint256 rewardAmount = $._rewardCalculator.calculateReward(
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(
delegator.weight, delegator.startedAt, delegator.endedAt, 0, 0
);
_reward(delegator.owner, rewardAmount);

emit DelegationEnded(delegationID, validationID, nonce);
}
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;
}
10 changes: 8 additions & 2 deletions contracts/staking/tests/ERC20TokenStakingManagerTests.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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";
Expand All @@ -23,11 +23,13 @@ contract ERC20TokenStakingManagerTest is PoSValidatorManagerTest {

ERC20TokenStakingManager public app;
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 @@ -38,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 @@ -84,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;

_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
11 changes: 8 additions & 3 deletions tests/flows/governance/validator_set_sig.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func ValidatorSetSig(network interfaces.LocalNetwork) {

// Construct a ValidatorSetSig message with mock ERC20 as the target contract
// and mint 100 tokens as the TxPayload
callData, err := erc20ABI.Pack("mint", big.NewInt(100))
callData, err := erc20ABI.Pack("mint", validatorSetSigContractAddress, big.NewInt(100))
Expect(err).Should(BeNil())

vssMessage1 := validatorsetsig.ValidatorSetSigMessage{
Expand All @@ -99,7 +99,7 @@ func ValidatorSetSig(network interfaces.LocalNetwork) {

// Construct a second ValidatorSetSig message with mock ERC20 as the target contract
// and mint 50 tokens as the TxPayload
callData2, err := erc20ABI.Pack("mint", big.NewInt(50))
callData2, err := erc20ABI.Pack("mint", validatorSetSigContractAddress, big.NewInt(50))
Expect(err).Should(BeNil())
vssMessage2 := validatorsetsig.ValidatorSetSigMessage{
ValidatorSetSigAddress: validatorSetSigContractAddress,
Expand All @@ -111,13 +111,18 @@ func ValidatorSetSig(network interfaces.LocalNetwork) {
}

// Create a message for the case where validatorSetSig contract and targetContract are on the same chain.
// Construct a ValidatorSetSig message with mock ERC20 as the target contract
// and mint 100 tokens as the TxPayload
callData3, err := erc20ABI.Pack("mint", validatorSetSigContractAddress2, big.NewInt(100))
Expect(err).Should(BeNil())

vssMessage3 := validatorsetsig.ValidatorSetSigMessage{
ValidatorSetSigAddress: validatorSetSigContractAddress2,
TargetContractAddress: exampleERC20ContractAddressB,
TargetBlockchainID: subnetB.BlockchainID,
Nonce: big.NewInt(0),
Value: big.NewInt(0),
Payload: callData,
Payload: callData3,
}

// Create chain config file with off-chain validatorsetsig message
Expand Down
10 changes: 8 additions & 2 deletions tests/flows/staking/poa_to_pos.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
poavalidatormanager "github.com/ava-labs/teleporter/abi-bindings/go/staking/PoAValidatorManager"
"github.com/ava-labs/teleporter/tests/interfaces"
"github.com/ava-labs/teleporter/tests/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"

. "github.com/onsi/gomega"
Expand Down Expand Up @@ -143,6 +142,13 @@ func PoAMigrationToPoS(network interfaces.LocalNetwork) {
)
Expect(err).Should(BeNil())

rewardCalculatorAddress, _ := utils.DeployExampleRewardCalculator(
ctx,
fundedKey,
subnetAInfo,
uint64(10),
)

tx, err = posValidatorManager.Initialize(
opts,
nativetokenstakingmanager.PoSValidatorManagerSettings{
Expand All @@ -154,7 +160,7 @@ func PoAMigrationToPoS(network interfaces.LocalNetwork) {
MinimumStakeAmount: big.NewInt(0).SetUint64(1e6),
MaximumStakeAmount: big.NewInt(0).SetUint64(10e6),
MinimumStakeDuration: uint64(24 * time.Hour),
RewardCalculator: common.Address{},
RewardCalculator: rewardCalculatorAddress,
},
)
Expect(err).Should(BeNil())
Expand Down
Loading

0 comments on commit 318c277

Please sign in to comment.