-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from iotexproject/feat/batch_claim_vault
add batch claim vault
- Loading branch information
Showing
4 changed files
with
203 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}) | ||
}) |