Skip to content

Commit

Permalink
feat(misc): examples: validator rewards with ERC20 minting (#1200)
Browse files Browse the repository at this point in the history
Co-authored-by: cryptoAtwill <[email protected]>
Co-authored-by: raulk <[email protected]>
Co-authored-by: raulk <[email protected]>
  • Loading branch information
4 people authored Nov 27, 2024
1 parent 4243ff9 commit ed602ce
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 0 deletions.
57 changes: 57 additions & 0 deletions contracts/contracts/examples/MintingValidatorRewarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.23;

import {IValidatorRewarder} from "../interfaces/IValidatorRewarder.sol";
import {Consensus} from "../structs/Activity.sol";
import {SubnetID} from "../structs/Subnet.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MintableERC20 is ERC20, Ownable {
constructor(string memory name, string memory symbol) ERC20(name, symbol) Ownable(msg.sender) {}

function mint(address recipient, uint256 amount) external onlyOwner {
_mint(recipient, amount);
}
}

/// An example validator rewarder implementation that mint ERC20 token for valiator
contract MintingValidatorRewarder is IValidatorRewarder, Ownable {
SubnetID public subnetId;
MintableERC20 public token;

constructor() Ownable(msg.sender) {
// We can also pass this address as a constructor parameter or update
// using a setter as well.
token = new MintableERC20("test", "TST");
}

function setSubnet(SubnetID calldata id) external onlyOwner {
require(id.route.length > 0, "root not allowed");

subnetId = id;
}

function notifyValidClaim(
SubnetID calldata id,
uint64 checkpointHeight,
Consensus.ValidatorData calldata data
) external override {
require(keccak256(abi.encode(id)) == keccak256(abi.encode(subnetId)), "not my subnet");

address actor = id.route[id.route.length - 1];
require(actor == msg.sender, "not from subnet");

uint256 reward = calculateReward(data, checkpointHeight);

token.mint(data.validator, reward);
}

/// @notice The internal method to derive the amount of reward that each validator should receive
/// based on their subnet activities
function calculateReward(Consensus.ValidatorData calldata data, uint64) internal pure returns (uint256) {
// Reward is the same as blocks mined for convenience.
return data.blocksCommitted;
}
}
99 changes: 99 additions & 0 deletions contracts/test/integration/SubnetActorDiamond.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGate

import {FullActivityRollup, Consensus} from "../../contracts/structs/Activity.sol";
import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol";
import {MintingValidatorRewarder} from "../../contracts/examples/MintingValidatorRewarder.sol";
import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol";
import {ActivityHelper} from "../helpers/ActivityHelper.sol";

Expand Down Expand Up @@ -2578,6 +2579,104 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase {
saDiamond.activity().batchSubnetClaim(subnetId, heights, claimProofs);
}

function testGatewayDiamond_ValidatorBatchClaimERC20Reward_Works() public {
MintingValidatorRewarder m = new MintingValidatorRewarder();
{
gatewayAddress = address(gatewayDiamond);

Asset memory source = Asset({kind: AssetKind.Native, tokenAddress: address(0)});

SubnetActorDiamond.ConstructorParams memory params = defaultSubnetActorParamsWith(
gatewayAddress,
SubnetID(ROOTNET_CHAINID, new address[](0)),
source,
AssetHelper.native()
);
params.validatorRewarder = address(m);
params.minValidators = 2;
params.permissionMode = PermissionMode.Federated;

saDiamond = createSubnetActor(params);
}

SubnetID memory subnetId = SubnetID(ROOTNET_CHAINID, new address[](1));
subnetId.route[0] = address(saDiamond);
m.setSubnet(subnetId);

(address[] memory addrs, uint256[] memory privKeys, bytes[] memory pubkeys) = TestUtils.newValidators(4);

{
uint256[] memory powers = new uint256[](4);
powers[0] = 10000;
powers[1] = 10000;
powers[2] = 10000;
powers[3] = 10000;
saDiamond.manager().setFederatedPower(addrs, pubkeys, powers);
}

bytes[] memory metadata = new bytes[](addrs.length);
uint64[] memory blocksMined = new uint64[](addrs.length);

// assign extra metadata to validator 0
// hardcode storageReward and uptimeReward to avoid stack too deep issues
metadata[0] = abi.encode(uint256(100), uint256(10));

blocksMined[0] = 11; // the first validator mined 11 blocks per checkpoint
blocksMined[1] = 2; // the second validator mined 2 blocks per checkpoint

(bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForConsensusActivity(
addrs,
blocksMined
);

(bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForConsensusActivity(
addrs,
blocksMined
);

// two checkpoints
confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot1));
confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot2));

Consensus.ValidatorClaim[] memory claimProofs = new Consensus.ValidatorClaim[](2);
uint64[] memory heights = new uint64[](2);

heights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod());
heights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod() * 2);

// Validator 0 claims 11 blocks per checkpoint = 22 tokens
claimProofs[0] = Consensus.ValidatorClaim({
data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}),
proof: ActivityHelper.wrapBytes32Array(proofs1[0])
});
claimProofs[1] = Consensus.ValidatorClaim({
data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}),
proof: ActivityHelper.wrapBytes32Array(proofs2[0])
});

vm.startPrank(addrs[0]);
vm.deal(addrs[0], 1 ether);
saDiamond.activity().batchSubnetClaim(subnetId, heights, claimProofs);

// Validator 1 claims 2 blocks per checkpoint = 4 tokens
claimProofs[0] = Consensus.ValidatorClaim({
data: Consensus.ValidatorData({validator: addrs[1], blocksCommitted: blocksMined[1]}),
proof: ActivityHelper.wrapBytes32Array(proofs1[1])
});
claimProofs[1] = Consensus.ValidatorClaim({
data: Consensus.ValidatorData({validator: addrs[1], blocksCommitted: blocksMined[1]}),
proof: ActivityHelper.wrapBytes32Array(proofs2[1])
});

vm.startPrank(addrs[1]);
vm.deal(addrs[1], 1 ether);
saDiamond.activity().batchSubnetClaim(subnetId, heights, claimProofs);

// Assert
assert(m.token().balanceOf(addrs[0]) == 22);
assert(m.token().balanceOf(addrs[1]) == 4);
}

// -----------------------------------------------------------------------------------------------------------------
// Tests for validator gater
// -----------------------------------------------------------------------------------------------------------------
Expand Down

0 comments on commit ed602ce

Please sign in to comment.