Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ interface IRollupCore {
function setRewardConfig(RewardConfig memory _config) external;
function updateManaTarget(uint256 _manaTarget) external;

function isRewardsClaimable() external view returns (bool);
// solhint-disable-next-line func-name-mixedcase
function L1_BLOCK_AT_GENESIS() external view returns (uint256);
}
Expand Down
75 changes: 75 additions & 0 deletions l1-contracts/src/periphery/FlushRewarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.27;

import {IInstance} from "@aztec/core/interfaces/IInstance.sol";
import {Errors} from "@aztec/core/libraries/Errors.sol";
import {Ownable} from "@oz/access/Ownable.sol";
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@oz/utils/math/Math.sol";
import {IFlushRewarder} from "./interfaces/IFlushRewarder.sol";

contract FlushRewarder is Ownable, IFlushRewarder {
using SafeERC20 for IERC20;

uint256 public constant DEFAULT_MAX_ADD_PER_FLUSH = 16;

IInstance public immutable ROLLUP;
IERC20 public immutable REWARD_ASSET;

uint256 public rewardPerInsertion;

uint256 internal debt;

mapping(address => uint256) public rewardsOf;

constructor(address _owner, IInstance _rollup, IERC20 _rewardAsset, uint256 _rewardPerInsertion) Ownable(_owner) {
ROLLUP = _rollup;
REWARD_ASSET = _rewardAsset;
rewardPerInsertion = _rewardPerInsertion;
}

function setRewardPerInsertion(uint256 _rewardPerInsertion) external override(IFlushRewarder) onlyOwner {
rewardPerInsertion = _rewardPerInsertion;
emit RewardPerInsertionUpdated(_rewardPerInsertion);
}

function recover(address _asset, address _to, uint256 _amount) external override(IFlushRewarder) onlyOwner {
if (_asset == address(REWARD_ASSET)) {
require(_amount <= rewardsAvailable(), InsufficientRewardsAvailable());
}
IERC20(_asset).safeTransfer(_to, _amount);
}

function flushEntryQueue() external override(IFlushRewarder) {
flushEntryQueue(DEFAULT_MAX_ADD_PER_FLUSH);
}

function claimRewards() external override(IFlushRewarder) {
require(ROLLUP.isRewardsClaimable(), Errors.Rollup__RewardsNotClaimable());

uint256 rewardsToClaim = rewardsOf[msg.sender];
if (rewardsToClaim > 0) {
rewardsOf[msg.sender] = 0;
debt -= rewardsToClaim;
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if the contract doesn't have sufficient rewards to cover the claim? On a related note, should we add a getter for the "shortfall" i.e. the amount of funds that would need to be added to this contract to cover all outstanding claims?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Nevermind- I missed the Math.min(insertions * rewardPerInsertion, rewardsAvailable()); 👍

REWARD_ASSET.safeTransfer(msg.sender, rewardsToClaim);
}
}

function flushEntryQueue(uint256 _toAdd) public override(IFlushRewarder) {
uint256 validatorSetSizeBefore = ROLLUP.getActiveAttesterCount();
ROLLUP.flushEntryQueue(_toAdd);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Feels like a previous design miss that this doesn't return the number flushed.

uint256 insertions = ROLLUP.getActiveAttesterCount() - validatorSetSizeBefore;

if (insertions > 0) {
uint256 rewards = Math.min(insertions * rewardPerInsertion, rewardsAvailable());
debt += rewards;
rewardsOf[msg.sender] += rewards;
}
}

function rewardsAvailable() public view override(IFlushRewarder) returns (uint256) {
return REWARD_ASSET.balanceOf(address(this)) - debt;
}
}
18 changes: 18 additions & 0 deletions l1-contracts/src/periphery/interfaces/IFlushRewarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.27;

interface IFlushRewarder {
event RewardPerInsertionUpdated(uint256 rewardPerInsertion);

error InsufficientRewardsAvailable();

function setRewardPerInsertion(uint256 _rewardPerInsertion) external;
function flushEntryQueue() external;
function flushEntryQueue(uint256 _toAdd) external;
function claimRewards() external;
function recover(address _asset, address _to, uint256 _amount) external;

function rewardsAvailable() external view returns (uint256);
function rewardsOf(address _account) external view returns (uint256);
}
121 changes: 121 additions & 0 deletions l1-contracts/test/staking/rewarded-flushing/rewardedFlushing.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: UNLICENSED
// solhint-disable func-name-mixedcase
// solhint-disable imports-order
// solhint-disable comprehensive-interface
// solhint-disable ordering

pragma solidity >=0.8.27;

import {StakingBase} from "../base.t.sol";
import {Errors} from "@aztec/core/libraries/Errors.sol";
import {Epoch, Timestamp} from "@aztec/shared/libraries/TimeMath.sol";
import {Status, AttesterView, IStakingCore} from "@aztec/core/interfaces/IStaking.sol";
import {Math} from "@oz/utils/math/Math.sol";
import {GSE, IGSECore} from "@aztec/governance/GSE.sol";
import {StakingQueueLib, DepositArgs} from "@aztec/core/libraries/StakingQueue.sol";
import {StakingQueueConfig, StakingQueueConfigLib} from "@aztec/core/libraries/compressed-data/StakingQueueConfig.sol";
import {Rollup} from "@aztec/core/Rollup.sol";
import {BN254Lib, G1Point, G2Point} from "@aztec/shared/libraries/BN254Lib.sol";
import {FlushRewarder, IFlushRewarder} from "@aztec/periphery/FlushRewarder.sol";
import {IInstance} from "@aztec/core/interfaces/IInstance.sol";

contract RewardedFlushingTest is StakingBase {
uint256 public constant MAX_QUEUE_FLUSH_SIZE = 48;
FlushRewarder public flushRewarder;

function setUp() public override {
super.setUp();
flushRewarder = new FlushRewarder(address(this), IInstance(address(staking)), stakingAsset, 50e18);

StakingQueueConfig memory stakingQueueConfig = StakingQueueConfig({
bootstrapValidatorSetSize: 0,
bootstrapFlushSize: 0,
normalFlushSizeMin: 8,
normalFlushSizeQuotient: 1,
maxQueueFlushSize: MAX_QUEUE_FLUSH_SIZE
});
Rollup rollup = Rollup(address(registry.getCanonicalRollup()));
vm.prank(rollup.owner());
rollup.updateStakingQueueConfig(stakingQueueConfig);

for (uint256 i = 1; i <= 10; i++) {
_help_deposit(address(uint160(i)), address(uint160(i)), true);
}
}

function test_givenNoFunding() external {
uint256 attestersBefore = staking.getActiveAttesterCount();
flushRewarder.flushEntryQueue();

assertEq(staking.getActiveAttesterCount(), attestersBefore + 8, "invalid active attester count");
assertEq(stakingAsset.balanceOf(address(flushRewarder)), 0, "invalid balance");
assertEq(flushRewarder.rewardsOf(address(this)), 0, "invalid rewards");
}

modifier givenFunding() {
vm.prank(stakingAsset.owner());
stakingAsset.mint(address(flushRewarder), 1_000_000e18);
_;
}

function test_givenRewardsNotClaimable() external givenFunding {
uint256 attestersBefore = staking.getActiveAttesterCount();
flushRewarder.flushEntryQueue();

assertEq(staking.getActiveAttesterCount(), attestersBefore + 8, "invalid active attester count");
assertEq(stakingAsset.balanceOf(address(flushRewarder)), 1_000_000e18, "invalid balance");
assertEq(flushRewarder.rewardsOf(address(this)), 8 * flushRewarder.rewardPerInsertion(), "invalid rewards");

vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__RewardsNotClaimable.selector));
flushRewarder.claimRewards();

vm.expectRevert(abi.encodeWithSelector(IFlushRewarder.InsufficientRewardsAvailable.selector));
flushRewarder.recover(address(stakingAsset), address(this), 1_000_000e18);
}

modifier givenRewardsClaimable() {
vm.prank(address(staking.getGSE().getGovernance()));
Rollup(address(staking)).setRewardsClaimable(true);
_;
}

function test_givenFundingGivenRewardsClaimable() external givenFunding givenRewardsClaimable {
uint256 attestersBefore = staking.getActiveAttesterCount();
flushRewarder.flushEntryQueue();

assertEq(staking.getActiveAttesterCount(), attestersBefore + 8, "invalid active attester count");
assertEq(stakingAsset.balanceOf(address(flushRewarder)), 1_000_000e18, "invalid balance");
uint256 rewards = flushRewarder.rewardsOf(address(this));
assertEq(rewards, 8 * flushRewarder.rewardPerInsertion(), "invalid rewards");

flushRewarder.claimRewards();
assertEq(stakingAsset.balanceOf(address(flushRewarder)), 1_000_000e18 - rewards, "invalid balance");
assertEq(flushRewarder.rewardsOf(address(this)), 0, "invalid rewards");
assertEq(stakingAsset.balanceOf(address(this)), rewards, "invalid balance");
}

function _help_deposit(address _attester, address _withdrawer, bool _moveWithLatestRollup) internal {
mint(address(this), ACTIVATION_THRESHOLD);
stakingAsset.approve(address(staking), ACTIVATION_THRESHOLD);
uint256 balance = stakingAsset.balanceOf(address(staking));

staking.deposit({
_attester: _attester,
_withdrawer: _withdrawer,
_publicKeyInG1: BN254Lib.g1Zero(),
_publicKeyInG2: BN254Lib.g2Zero(),
_proofOfPossession: BN254Lib.g1Zero(),
_moveWithLatestRollup: _moveWithLatestRollup
});

assertEq(stakingAsset.balanceOf(address(staking)), balance + ACTIVATION_THRESHOLD, "invalid balance");

DepositArgs memory validator = staking.getEntryQueueAt(staking.getEntryQueueLength() - 1);
assertEq(validator.attester, _attester, "invalid attester");
assertEq(validator.withdrawer, _withdrawer, "invalid withdrawer");
assertTrue(BN254Lib.isZero(validator.publicKeyInG1), "invalid public key in G1");
assertTrue(BN254Lib.isZero(validator.publicKeyInG2), "invalid public key in G2");
assertTrue(BN254Lib.isZero(validator.proofOfPossession), "invalid proof of possession");
assertEq(validator.moveWithLatestRollup, _moveWithLatestRollup, "invalid move with latest rollup");
}
}