Skip to content

Commit

Permalink
Merge pull request #20 from iotexproject/feat/batch_claim_vault
Browse files Browse the repository at this point in the history
add batch claim vault
  • Loading branch information
ququzone authored Aug 30, 2024
2 parents 11670da + a2bcbb2 commit 65b2ec2
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 0 deletions.
106 changes: 106 additions & 0 deletions contracts/BatchClaimVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract BatchClaimVault is OwnableUpgradeable {
event Donation(address indexed donor, uint256 amount);
event Withdraw(address indexed owner, address recipcient, uint256 amount);
event Initialize(uint256 batchSize, uint256 rewardPerBlock);
event AddProject(uint256 projectId, address recipient, uint256 startBlock);
event RemoveProject(uint256 projectId);
event Claim(uint256 projectId, address recipient, uint256 startBlock, uint256 blocks, uint256 rewards);
event ChangeBatchSize(uint256 batchSize);
event ChangeRewardPerBlock(uint256 rewardPerBlock);
event ChangeRecipient(address indexed admin, uint256 projectId, address recipient);

uint256 public batchSize;
uint256 public rewardPerBlock;
uint256 public projectNum;
mapping(uint256 => address) public projectRecipient;
mapping(uint256 => uint256) public lastClaimedBlock;

function initialize(uint256 _rewardPerBlock) public initializer {
require(_rewardPerBlock > 0, "invalid reward per block");

__Ownable_init_unchained();

batchSize = 17280;
rewardPerBlock = _rewardPerBlock;

emit Initialize(17280, rewardPerBlock);
}

function addProject(uint256 _projectId, address _recipient, uint256 _startBlock) external onlyOwner {
require(_recipient != address(0), "zero address");
require(_startBlock > block.number, "invalid start block");

projectNum++;
projectRecipient[_projectId] = _recipient;
lastClaimedBlock[_projectId] = _startBlock;
emit AddProject(_projectId, _recipient, _startBlock);
}

function removeProject(uint256 _projectId) external onlyOwner {
require(projectRecipient[_projectId] != address(0), "invalid project");

delete projectRecipient[_projectId];
delete lastClaimedBlock[_projectId];
emit RemoveProject(_projectId);
}

function claim(uint256 _projectId) external returns (uint256) {
uint256 _lastClaimedBlock = lastClaimedBlock[_projectId];
require(_lastClaimedBlock != 0, "invalid project");
require(_lastClaimedBlock + batchSize <= block.number, "claim too short");

uint256 _claimableBlocks = ((block.number - _lastClaimedBlock) / batchSize) * batchSize;
uint256 _rewards = _claimableBlocks * rewardPerBlock;

require(address(this).balance >= _rewards, "insufficient fund");
lastClaimedBlock[_projectId] += _claimableBlocks;

address _recipient = projectRecipient[_projectId];
(bool success, ) = payable(_recipient).call{value: _rewards}("");
require(success, "transfer rewards failed");

emit Claim(_projectId, _recipient, _lastClaimedBlock, _claimableBlocks, _rewards);
return _rewards;
}

function changeBatchSize(uint256 _batchSize) external onlyOwner {
require(_batchSize > 0, "invalid batch size");
batchSize = _batchSize;

emit ChangeBatchSize(_batchSize);
}

function changeRewardPerBlock(uint256 _rewardPerBlock) external onlyOwner {
require(_rewardPerBlock > 0, "invalid reward per block");
rewardPerBlock = _rewardPerBlock;

emit ChangeRewardPerBlock(_rewardPerBlock);
}

function changeRecipient(uint256 _projectId, address _recipient) external {
require(_recipient != address(0), "zero address");
require(msg.sender == owner() || msg.sender == projectRecipient[_projectId], "invalid admin");

projectRecipient[_projectId] = _recipient;
emit ChangeRecipient(msg.sender, _projectId, _recipient);
}

receive() external payable {
emit Donation(msg.sender, msg.value);
}

function donate() external payable {
emit Donation(msg.sender, msg.value);
}

function withdraw(address payable _recipcient, uint256 _amount) external onlyOwner {
(bool success, ) = payable(_recipcient).call{value: _amount}("");
require(success, "withdraw token failed.");
emit Withdraw(msg.sender, _recipcient, _amount);
}
}
20 changes: 20 additions & 0 deletions script/10_deploy_claim_vault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ethers, upgrades } from 'hardhat';
require('dotenv').config();

async function main() {
const vault = await upgrades.deployProxy(
await ethers.getContractFactory('BatchClaimVault'),
[ethers.parseEther('0.1')],
{
initializer: 'initialize',
},
);
await vault.waitForDeployment();

console.log(`BatchClaimVault deployed to ${vault.target}`);
}

main().catch(err => {
console.error(err);
process.exitCode = 1;
});
18 changes: 18 additions & 0 deletions script/upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ethers, upgrades } from 'hardhat';

async function main() {
if (process.env.CLAIM_VAULT) {
console.log(`upgrade claim vault`);
const vault = await ethers.getContractFactory('BatchClaimVault');
await upgrades.forceImport(process.env.CLAIM_VAULT, vault);
await upgrades.upgradeProxy(process.env.CLAIM_VAULT, vault, {
redeployImplementation: 'always',
});
console.log(`Upgrade BatchClaimVault ${process.env.CLAIM_VAULT} successfull!`);
}
}

main().catch(err => {
console.error(err);
process.exitCode = 1;
});
59 changes: 59 additions & 0 deletions test/TestClaimVault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect } from 'chai';
import { ethers } from 'hardhat';
import { time, mine, mineUpTo } from "@nomicfoundation/hardhat-toolbox/network-helpers";

describe('BatchClaimVault tests', function () {
it('claim', async function () {
const [owner, projectRecipient, fakeRecipient] = await ethers.getSigners();

const vault = await ethers.deployContract('BatchClaimVault');
await vault.connect(owner).initialize(ethers.parseEther('0.1'));

expect(await vault.batchSize()).to.equal(17280);
await expect(vault.connect(fakeRecipient).changeBatchSize(10))
.to.revertedWith('Ownable: caller is not the owner');
await expect(vault.connect(fakeRecipient).changeRewardPerBlock(10))
.to.revertedWith('Ownable: caller is not the owner');
await expect(vault.connect(fakeRecipient).changeRecipient(1, fakeRecipient.address))
.to.revertedWith('invalid admin');

await vault.connect(owner).changeBatchSize(10);

const projectId = 1;
await expect(vault.connect(projectRecipient).claim(projectId)).to.rejectedWith('invalid project');
const startBlock = (await time.latestBlock() + 2);
await vault.connect(owner).addProject(projectId, projectRecipient.address, startBlock);

await expect(vault.connect(owner).claim(projectId)).to.rejectedWith('claim too short');
await expect(vault.connect(projectRecipient).claim(projectId)).to.rejectedWith('claim too short');

await mine(10);
await expect(vault.connect(projectRecipient).claim(projectId)).to.rejectedWith('insufficient fund');

await vault.donate({value: ethers.parseEther("1")});
let claimBeforeBalance = await ethers.provider.getBalance(projectRecipient.address);
let tx = await vault.connect(projectRecipient).claim(projectId);
let claimAfterBalance = await ethers.provider.getBalance(projectRecipient.address);
let receipt = await ethers.provider.getTransactionReceipt(tx.hash);
expect(claimBeforeBalance).to.equals(claimAfterBalance - ethers.parseEther("1") + receipt!.gasUsed * receipt!.gasPrice);

await expect(vault.connect(projectRecipient).claim(projectId)).to.rejectedWith('claim too short');

const lastClaimedBlock = await vault.lastClaimedBlock(projectId);
await mineUpTo(lastClaimedBlock + BigInt(10));

await expect(vault.connect(projectRecipient).claim(projectId)).to.rejectedWith('insufficient fund');
await vault.donate({value: ethers.parseEther("1")});
claimBeforeBalance = await ethers.provider.getBalance(projectRecipient.address);
tx = await vault.connect(projectRecipient).claim(projectId);
claimAfterBalance = await ethers.provider.getBalance(projectRecipient.address);
receipt = await ethers.provider.getTransactionReceipt(tx.hash);
expect(claimBeforeBalance).to.equals(claimAfterBalance - ethers.parseEther("1") + receipt!.gasUsed * receipt!.gasPrice);

await vault.removeProject(projectId);
await expect(vault.connect(projectRecipient).claim(projectId)).to.rejectedWith('invalid project');

expect(await vault.lastClaimedBlock(projectId)).to.equals(0);
expect(await vault.projectRecipient(projectId)).to.equals('0x0000000000000000000000000000000000000000');
})
})

0 comments on commit 65b2ec2

Please sign in to comment.