Skip to content

Commit

Permalink
feat: Add fee.
Browse files Browse the repository at this point in the history
  • Loading branch information
clement-ux committed Dec 20, 2024
1 parent 03eb2d6 commit 8aeab0e
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 31 deletions.
105 changes: 76 additions & 29 deletions contracts/contracts/strategies/CurvePoolBooster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,28 @@ import { Initializable } from "../utils/Initializable.sol";
import { ICampaingRemoteManager } from "../interfaces/ICampaignRemoteManager.sol";

contract CurvePoolBooster is Initializable, Governable {
////////////////////////////////////////////////////
/// --- CONSTANTS && IMMUTABLES
////////////////////////////////////////////////////
uint16 public constant BASE_FEE = 10_000; // 100%
address public immutable gauge;
address public immutable rewardToken;
address public immutable campaignRemoteManager;
uint256 public immutable targetChainId;

////////////////////////////////////////////////////
/// --- STORAGE
////////////////////////////////////////////////////
uint16 public fee;
address public operator;
address public feeCollector;
uint256 public campaignId;

////////////////////////////////////////////////////
/// --- EVENTS
////////////////////////////////////////////////////
event FeeSet(uint16 _newFee);
event FeeCollectorSet(address _newFeeCollector);
event CampaignIdSet(uint256 _newId);
event OperatorSet(address _newOperator);
event BribeCreated(
Expand All @@ -28,6 +42,9 @@ contract CurvePoolBooster is Initializable, Governable {
event RewardPerVoteManaged(uint256 newMaxRewardPerVote);
event RescueTokens(address token, uint256 amount, address receiver);

////////////////////////////////////////////////////
/// --- MODIFIERS
////////////////////////////////////////////////////
modifier onlyOperator() {
require(
msg.sender == operator || isGovernor(),
Expand All @@ -36,6 +53,9 @@ contract CurvePoolBooster is Initializable, Governable {
_;
}

////////////////////////////////////////////////////
/// --- CONSTRUCTOR && INITIALIZATION
////////////////////////////////////////////////////
constructor(
uint256 _targetChainId,
address _campaignRemoteManager,
Expand All @@ -51,10 +71,19 @@ contract CurvePoolBooster is Initializable, Governable {
_setGovernor(address(0));
}

function initialize(address _operator) external initializer {
function initialize(
address _operator,
uint16 _fee,
address _feeCollector
) external initializer {
operator = _operator;
fee = _fee;
feeCollector = _feeCollector;
}

////////////////////////////////////////////////////
/// --- MUTATIVE FUNCTIONS
////////////////////////////////////////////////////
function createCampaign(
uint8 numberOfPeriods,
uint256 maxRewardPerVote,
Expand All @@ -65,12 +94,18 @@ contract CurvePoolBooster is Initializable, Governable {
require(campaignId == 0, "Campaign already created");

// Cache current rewardToken balance
uint256 totalRewardAmount = IERC20(rewardToken).balanceOf(
address(this)
);
uint256 balance = IERC20(rewardToken).balanceOf(address(this));
require(balance > 0, "No reward to bribe");

// Approve the total reward amount to the campaign manager
IERC20(rewardToken).approve(campaignRemoteManager, totalRewardAmount);
// Handle fee (if any)
uint256 feeAmount = (balance * fee) / BASE_FEE;
if (feeAmount > 0) {
balance -= feeAmount;
IERC20(rewardToken).transfer(feeCollector, feeAmount);
}

// Approve the balance to the campaign manager
IERC20(rewardToken).approve(campaignRemoteManager, balance);

// Create a new campaign
ICampaingRemoteManager(campaignRemoteManager).createCampaign{
Expand All @@ -83,7 +118,7 @@ contract CurvePoolBooster is Initializable, Governable {
rewardToken: rewardToken,
numberOfPeriods: numberOfPeriods,
maxRewardPerVote: maxRewardPerVote,
totalRewardAmount: totalRewardAmount,
totalRewardAmount: balance,
addresses: blacklist,
hook: address(0),
isWhitelist: false
Expand All @@ -92,12 +127,7 @@ contract CurvePoolBooster is Initializable, Governable {
additionalGasLimit
);

emit BribeCreated(
gauge,
rewardToken,
maxRewardPerVote,
totalRewardAmount
);
emit BribeCreated(gauge, rewardToken, maxRewardPerVote, balance);
}

function manageTotalRewardAmount(
Expand All @@ -107,18 +137,18 @@ contract CurvePoolBooster is Initializable, Governable {
require(campaignId != 0, "Campaign not created");

// Cache current rewardToken balance
uint256 extraTotalRewardAmount = IERC20(rewardToken).balanceOf(
address(this)
);
uint256 balance = IERC20(rewardToken).balanceOf(address(this));
require(balance > 0, "No reward to manage");

// Approve the total reward amount to the campaign manager
require(extraTotalRewardAmount > 0, "No reward to manage");
// Handle fee (if any)
uint256 feeAmount = (balance * fee) / BASE_FEE;
if (feeAmount > 0) {
balance -= feeAmount;
IERC20(rewardToken).transfer(feeCollector, feeAmount);

Check warning on line 147 in contracts/contracts/strategies/CurvePoolBooster.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/strategies/CurvePoolBooster.sol#L146-L147

Added lines #L146 - L147 were not covered by tests
}

// Approve the total reward amount to the campaign manager
IERC20(rewardToken).approve(
campaignRemoteManager,
extraTotalRewardAmount
);
IERC20(rewardToken).approve(campaignRemoteManager, balance);

// Manage the campaign
ICampaingRemoteManager(campaignRemoteManager).manageCampaign{
Expand All @@ -128,14 +158,14 @@ contract CurvePoolBooster is Initializable, Governable {
campaignId: campaignId,
rewardToken: rewardToken,
numberOfPeriods: 0,
totalRewardAmount: extraTotalRewardAmount,
totalRewardAmount: balance,
maxRewardPerVote: 0
}),
targetChainId,
additionalGasLimit
);

emit TotalRewardAmountManaged(extraTotalRewardAmount);
emit TotalRewardAmountManaged(balance);
}

function manageNumberOfPeriods(
Expand All @@ -144,7 +174,9 @@ contract CurvePoolBooster is Initializable, Governable {
uint256 additionalGasLimit
) external onlyOperator {
require(campaignId != 0, "Campaign not created");
require(extraNumberOfPeriods > 0, "Invalid number of periods");

// Manage the campaign
ICampaingRemoteManager(campaignRemoteManager).manageCampaign{
value: bridgeFee
}(
Expand All @@ -168,7 +200,9 @@ contract CurvePoolBooster is Initializable, Governable {
uint256 additionalGasLimit
) external onlyOperator {
require(campaignId != 0, "Campaign not created");
require(newMaxRewardPerVote > 0, "Invalid reward per vote");

// Manage the campaign
ICampaingRemoteManager(campaignRemoteManager).manageCampaign{
value: bridgeFee
}(
Expand All @@ -186,16 +220,14 @@ contract CurvePoolBooster is Initializable, Governable {
emit RewardPerVoteManaged(newMaxRewardPerVote);
}

////////////////////////////////////////////////////
/// --- GOVERNANCE && OPERATION
////////////////////////////////////////////////////
function setCampaignId(uint256 _campaignId) external onlyOperator {
campaignId = _campaignId;
emit CampaignIdSet(_campaignId);
}

function setOperator(address _newOperator) external onlyGovernor {
operator = _newOperator;
emit OperatorSet(_newOperator);
}

function sendETH(address receiver) external onlyOperator {
emit RescueTokens(address(0), address(this).balance, receiver);
payable(receiver).transfer(address(this).balance);

Check warning on line 233 in contracts/contracts/strategies/CurvePoolBooster.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/strategies/CurvePoolBooster.sol#L232-L233

Added lines #L232 - L233 were not covered by tests
Expand All @@ -211,5 +243,20 @@ contract CurvePoolBooster is Initializable, Governable {
IERC20(token).transfer(receiver, balance);

Check warning on line 243 in contracts/contracts/strategies/CurvePoolBooster.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/strategies/CurvePoolBooster.sol#L241-L243

Added lines #L241 - L243 were not covered by tests
}

function setOperator(address _newOperator) external onlyGovernor {
operator = _newOperator;
emit OperatorSet(_newOperator);
}

function setFee(uint16 _fee) external onlyGovernor {
require(_fee <= BASE_FEE / 2, "Fee too high");
fee = _fee;
}

function setFeeCollector(address _feeCollector) external onlyGovernor {
feeCollector = _feeCollector;
emit FeeCollectorSet(_feeCollector);
}

receive() external payable {}
}
4 changes: 2 additions & 2 deletions contracts/deploy/mainnet/114_pool_booster_curve.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ module.exports = deploymentWithGovernanceProposal(

// 3. Initialize
const initData = cCurvePoolBooster.interface.encodeFunctionData(
"initialize(address)",
[deployerAddr]
"initialize(address,uint16,address)",
[deployerAddr, 0, deployerAddr]
);

// 4. Initialize proxy
Expand Down
64 changes: 64 additions & 0 deletions contracts/test/strategies/curvePoolBooster.mainnet.fork-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,46 @@ describe("ForkTest: CurvePoolBooster", function () {
);
});

it("Should Create a campaign with fee", async () => {
const { curvePoolBooster, oeth, woeth, josh } = fixture;
const { deployerAddr } = await getNamedAccounts();
const woethSigner = await impersonateAndFund(woeth.address);
const sDeployer = await ethers.provider.getSigner(deployerAddr);
const gov = await curvePoolBooster.governor();
const sGov = await ethers.provider.getSigner(gov);

// Deal OETH and ETH to pool booster
await oeth
.connect(woethSigner)
.transfer(curvePoolBooster.address, parseUnits("10"));
expect(await oeth.balanceOf(curvePoolBooster.address)).to.equal(
parseUnits("10")
);
await sDeployer.sendTransaction({
to: curvePoolBooster.address,
value: parseUnits("1"),
});

// Set fee and fee collector
await curvePoolBooster.connect(sGov).setFee(1000); // 10%
await curvePoolBooster.connect(sGov).setFeeCollector(josh.address);
expect(await oeth.balanceOf(josh.address)).to.equal(0);

// Create campaign
await curvePoolBooster
.connect(sDeployer)
.createCampaign(
4,
10,
[addresses.mainnet.ConvexVoter],
parseUnits("0.1"),
100
);

// Ensure fee is collected
expect(await oeth.balanceOf(josh.address)).to.equal(parseUnits("1"));
});

it("Should set campaign id", async () => {
const { curvePoolBooster } = fixture;
const { deployerAddr } = await getNamedAccounts();
Expand All @@ -80,6 +120,30 @@ describe("ForkTest: CurvePoolBooster", function () {
expect(await curvePoolBooster.campaignId()).to.equal(12);
});

it("Should set fee and fee collector", async () => {
const { curvePoolBooster, josh } = fixture;
const gov = await curvePoolBooster.governor();
const sGov = await ethers.provider.getSigner(gov);
expect(await curvePoolBooster.fee()).to.equal(0);

await curvePoolBooster.connect(sGov).setFee(100);
expect(await curvePoolBooster.fee()).to.equal(100);

expect(await curvePoolBooster.feeCollector()).not.to.equal(josh.address);
await curvePoolBooster.connect(sGov).setFeeCollector(josh.address);
expect(await curvePoolBooster.feeCollector()).to.equal(josh.address);
});

it("Should set operator", async () => {
const { curvePoolBooster, josh } = fixture;
const gov = await curvePoolBooster.governor();
const sGov = await ethers.provider.getSigner(gov);

expect(await curvePoolBooster.operator()).not.to.equal(josh.address);
await curvePoolBooster.connect(sGov).setOperator(josh.address);
expect(await curvePoolBooster.operator()).to.equal(josh.address);
});

it("Should manage total rewards", async () => {
const { curvePoolBooster, oeth, woeth } = fixture;
const { deployerAddr } = await getNamedAccounts();
Expand Down

0 comments on commit 8aeab0e

Please sign in to comment.