From 75904625d694d0625043aae7878705cfbb7c39b1 Mon Sep 17 00:00:00 2001 From: belbix <7453635@gmail.com> Date: Sat, 26 Aug 2023 11:16:05 +0300 Subject: [PATCH] rewards redirector --- contracts/tools/RewardsRedirector.sol | 99 ++++++++++++++ scripts/addresses/polygon.ts | 1 + scripts/deploy/DeployRewardsRedirector.ts | 20 +++ test/tools/RewardsRedirectorTest.ts | 157 ++++++++++++++++++++++ 4 files changed, 277 insertions(+) create mode 100644 contracts/tools/RewardsRedirector.sol create mode 100644 scripts/deploy/DeployRewardsRedirector.ts create mode 100644 test/tools/RewardsRedirectorTest.ts diff --git a/contracts/tools/RewardsRedirector.sol b/contracts/tools/RewardsRedirector.sol new file mode 100644 index 0000000..95b4569 --- /dev/null +++ b/contracts/tools/RewardsRedirector.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.17; + +import "../openzeppelin/SafeERC20.sol"; +import "../interfaces/IGauge.sol"; +import "../openzeppelin/EnumerableSet.sol"; + +contract RewardsRedirector { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + string public constant VERSION = "1.0.0"; + + address public owner; + address public pendingOwner; + address public gauge; + EnumerableSet.AddressSet internal operators; + EnumerableSet.AddressSet internal redirected; + mapping(address => address[]) public redirectedVaults; + + constructor(address _owner, address _gauge) { + owner = _owner; + gauge = _gauge; + operators.add(_owner); + } + + modifier onlyOwner() { + require(msg.sender == owner, "!owner"); + _; + } + + modifier onlyOperator() { + require(operators.contains(msg.sender), "!operator"); + _; + } + + /////////////////// VIEWS //////////////////// + + function getOperators() external view returns (address[] memory) { + return operators.values(); + } + + function getRedirected() external view returns (address[] memory) { + return redirected.values(); + } + + function getRedirectedVaults(address adr) external view returns (address[] memory) { + return redirectedVaults[adr]; + } + + /////////////////// GOV //////////////////// + + function offerOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), "zero"); + pendingOwner = newOwner; + } + + function acceptOwnership() external { + require(msg.sender == pendingOwner, "!owner"); + owner = pendingOwner; + } + + function changeOperator(address adr, bool status) external onlyOwner { + if (status) { + operators.add(adr); + } else { + operators.remove(adr); + } + } + + function changeRedirected(address adr, address[] calldata vaults, bool status) external onlyOwner { + if (status) { + redirected.add(adr); + redirectedVaults[adr] = vaults; + } else { + redirected.remove(adr); + delete redirectedVaults[adr]; + } + } + + /////////////////// MAIN LOGIC //////////////////// + + function claimRewards() external onlyOperator { + address _gauge = gauge; + address[] memory _redirected = redirected.values(); + for (uint j; j < _redirected.length; ++j) { + address[] memory _vaults = redirectedVaults[_redirected[j]]; + for (uint i; i < _vaults.length; ++i) { + IGauge(_gauge).getAllRewards(_vaults[i], _redirected[j]); + } + } + } + + function withdraw(address token) external onlyOperator { + IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this))); + } + +} diff --git a/scripts/addresses/polygon.ts b/scripts/addresses/polygon.ts index 009e24e..ff53a6b 100644 --- a/scripts/addresses/polygon.ts +++ b/scripts/addresses/polygon.ts @@ -36,6 +36,7 @@ export class PolygonAddresses { public static SPLITTER_REBALANCE_RESOLVER = "0x9618D3e9c8a133d081dE37b99c6ECf108C1e82F2".toLowerCase(); public static PERF_FEE_TREASURY = "0x9Cc199D4353b5FB3e6C8EEBC99f5139e0d8eA06b".toLowerCase(); public static TETU_BRIDGED_PROCESSING = "0x1950a09fc28Dd3C36CaC89485357844Af0739C07".toLowerCase(); + public static REWARDS_REDIRECTOR = "0xA9947d0815d6EA3077805E2112FB19572DD4dc9E".toLowerCase(); // PROTOCOL ADRS public static DEPOSIT_HELPER_V2 = "0xab2422A4d8Ac985AE98F5Da3713988b420f24165".toLowerCase(); diff --git a/scripts/deploy/DeployRewardsRedirector.ts b/scripts/deploy/DeployRewardsRedirector.ts new file mode 100644 index 0000000..96a21f1 --- /dev/null +++ b/scripts/deploy/DeployRewardsRedirector.ts @@ -0,0 +1,20 @@ +import {ethers} from "hardhat"; +import {DeployerUtils} from "../utils/DeployerUtils"; +import {appendFileSync} from "fs"; +import {Addresses} from "../addresses/addresses"; +import {ForwarderDistributeResolver__factory, HardWorkResolver__factory} from "../../typechain"; +import {RunHelper} from "../utils/RunHelper"; + + +async function main() { + const signer = (await ethers.getSigners())[0]; + const core = Addresses.getCore(); + await DeployerUtils.deployContract(signer, 'RewardsRedirector', '0x0644141dd9c2c34802d28d334217bd2034206bf7', core.gauge); +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); diff --git a/test/tools/RewardsRedirectorTest.ts b/test/tools/RewardsRedirectorTest.ts new file mode 100644 index 0000000..cb5d38a --- /dev/null +++ b/test/tools/RewardsRedirectorTest.ts @@ -0,0 +1,157 @@ +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; +import {MockStakingToken, MockToken, MultiGauge, RewardsRedirector, VeTetu,} from "../../typechain"; +import {TimeUtils} from "../TimeUtils"; +import {ethers} from "hardhat"; +import {DeployerUtils} from "../../scripts/utils/DeployerUtils"; +import {BigNumber} from "ethers"; +import {Misc} from "../../scripts/utils/Misc"; +import {parseUnits} from "ethers/lib/utils"; + +const {expect} = chai; +chai.use(chaiAsPromised); + +const FULL_AMOUNT = parseUnits('100'); + +describe("RewardsRedirectorTest", function () { + let snapshotBefore: string; + let snapshot: string; + let owner: SignerWithAddress; + let rewarder: SignerWithAddress; + let user: SignerWithAddress; + let claimer: SignerWithAddress; + + let redirector: RewardsRedirector; + let stakingToken: MockStakingToken; + let stakingToken2: MockStakingToken; + let tetu: MockToken; + let rewardToken: MockToken; + let rewardToken2: MockToken; + let rewardTokenDefault: MockToken; + let gauge: MultiGauge; + let ve: VeTetu; + + + before(async function () { + this.timeout(1200000); + snapshotBefore = await TimeUtils.snapshot(); + [owner, rewarder, user, claimer] = await ethers.getSigners(); + + tetu = await DeployerUtils.deployMockToken(owner, 'TETU', 18); + const controller = await DeployerUtils.deployMockController(owner); + ve = await DeployerUtils.deployVeTetu(owner, tetu.address, controller.address); + const voter = await DeployerUtils.deployMockVoter(owner, ve.address); + await controller.setVoter(voter.address); + + rewardToken = await DeployerUtils.deployMockToken(owner, 'REWARD', 18); + rewardToken = await DeployerUtils.deployMockToken(owner, 'REWARD', 18); + await rewardToken.mint(rewarder.address, BigNumber.from(Misc.MAX_UINT).sub(parseUnits('1000000'))); + rewardToken2 = await DeployerUtils.deployMockToken(owner, 'REWARD2', 18); + await rewardToken2.mint(rewarder.address, parseUnits('100')); + rewardTokenDefault = await DeployerUtils.deployMockToken(owner, 'REWARD_DEFAULT', 18); + await rewardTokenDefault.mint(rewarder.address, parseUnits('100')); + + gauge = await DeployerUtils.deployMultiGauge( + owner, + controller.address, + ve.address, + rewardTokenDefault.address, + ); + + stakingToken = await DeployerUtils.deployMockStakingToken(owner, gauge.address, 'VAULT', 18); + stakingToken2 = await DeployerUtils.deployMockStakingToken(owner, gauge.address, 'VAULT2', 18); + + await gauge.addStakingToken(stakingToken.address); + await gauge.registerRewardToken(stakingToken.address, rewardToken.address); + + await stakingToken.mint(owner.address, FULL_AMOUNT); + await stakingToken.mint(user.address, FULL_AMOUNT); + + await tetu.approve(ve.address, Misc.MAX_UINT); + await tetu.connect(user).approve(ve.address, Misc.MAX_UINT); + await tetu.connect(rewarder).approve(ve.address, Misc.MAX_UINT); + await rewardToken.approve(gauge.address, Misc.MAX_UINT); + await rewardToken2.approve(gauge.address, Misc.MAX_UINT); + await rewardTokenDefault.approve(gauge.address, Misc.MAX_UINT); + await rewardToken.connect(rewarder).approve(gauge.address, Misc.MAX_UINT); + await rewardToken2.connect(rewarder).approve(gauge.address, Misc.MAX_UINT); + await rewardTokenDefault.connect(rewarder).approve(gauge.address, Misc.MAX_UINT); + + redirector = await DeployerUtils.deployContract(owner, "RewardsRedirector", owner.address, gauge.address) as RewardsRedirector; + }) + + after(async function () { + await TimeUtils.rollback(snapshotBefore); + }); + + beforeEach(async function () { + snapshot = await TimeUtils.snapshot(); + }); + + afterEach(async function () { + await TimeUtils.rollback(snapshot); + }); + + it("set new gov", async () => { + await expect(redirector.connect(user).offerOwnership(user.address)).revertedWith('!owner'); + await redirector.offerOwnership(user.address) + await expect(redirector.acceptOwnership()).revertedWith('!owner'); + await redirector.connect(user).acceptOwnership() + expect(await redirector.owner()).eq(user.address) + await expect(redirector.offerOwnership(user.address)).revertedWith('!owner'); + }) + + it("change operator test", async () => { + expect((await redirector.getOperators())[0]).eq(owner.address); + + await redirector.changeOperator(user.address, true); + await redirector.changeOperator(rewarder.address, true); + + expect((await redirector.getOperators())[1]).eq(user.address); + expect((await redirector.getOperators())[2]).eq(rewarder.address); + + await redirector.changeOperator(user.address, false); + expect((await redirector.getOperators())[1]).eq(rewarder.address); + }) + + it("change redirect test", async () => { + expect((await redirector.getRedirected()).length).eq(0); + + await redirector.changeRedirected(user.address, [owner.address, rewarder.address], true); + + expect((await redirector.getRedirected())[0]).eq(user.address); + expect((await redirector.getRedirectedVaults(user.address))[0]).eq(owner.address); + expect((await redirector.getRedirectedVaults(user.address))[1]).eq(rewarder.address); + + await redirector.changeRedirected(user.address, [], false); + expect((await redirector.getRedirected()).length).eq(0); + expect((await redirector.getRedirectedVaults(user.address)).length).eq(0); + }) + + it("claim test", async () => { + // add reward + await gauge.notifyRewardAmount(stakingToken.address, rewardToken.address, parseUnits('1')); + await gauge.registerRewardToken(stakingToken.address, rewardToken2.address); + await gauge.notifyRewardAmount(stakingToken.address, rewardToken2.address, parseUnits('1')); + + // redirect + await gauge.setRewardsRedirect(user.address, redirector.address); + await redirector.changeRedirected(user.address, [stakingToken.address], true); + + await TimeUtils.advanceBlocksOnTs(60 * 60 * 24 * 4) + + expect(await rewardToken.balanceOf(user.address)).eq(0); + expect(await rewardToken.balanceOf(redirector.address)).eq(0); + + await redirector.claimRewards() + + expect(await rewardToken.balanceOf(user.address)).eq(0); + expect(await rewardToken.balanceOf(redirector.address)).not.eq(0); + + await redirector.changeOperator(claimer.address, true); + expect(await rewardToken.balanceOf(claimer.address)).eq(0); + await redirector.connect(claimer).withdraw(rewardToken.address); + expect(await rewardToken.balanceOf(claimer.address)).not.eq(0); + }) +})