Skip to content

Commit

Permalink
Merge pull request #21 from iotexproject/feat/time_batch_claim_vault
Browse files Browse the repository at this point in the history
add PeriodClaimVault
  • Loading branch information
ququzone authored Sep 3, 2024
2 parents 023165d + c175be0 commit ca52db4
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
2 changes: 2 additions & 0 deletions contracts/BatchClaimVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ contract BatchClaimVault is OwnableUpgradeable {
function addProject(uint256 _projectId, address _recipient, uint256 _startBlock) external onlyOwner {
require(_recipient != address(0), "zero address");
require(_startBlock > block.number, "invalid start block");
require(projectRecipient[_projectId] == address(0), "already added");

projectNum++;
projectRecipient[_projectId] = _recipient;
Expand All @@ -46,6 +47,7 @@ contract BatchClaimVault is OwnableUpgradeable {

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

Expand Down
125 changes: 125 additions & 0 deletions contracts/PeriodClaimVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

interface IioIDStore {
function projectDeviceContract(uint256 _projectId) external view returns (address);
function projectActivedAmount(uint256 _projectId) external view returns (uint256);
}

contract PeriodClaimVault is OwnableUpgradeable {
event Donation(address indexed donor, uint256 amount);
event Withdraw(address indexed owner, address recipcient, uint256 amount);
event Initialize(uint256 period, uint256 rewardPerDevice);
event AddProject(uint256 projectId, address recipient, uint256 startTimestamp);
event RemoveProject(uint256 projectId);
event Claim(uint256 projectId, address recipient, uint256 startTimestamp, uint256 endTimestamp, uint256 rewards);
event ChangePeriod(uint256 batchSize);
event ChangeRewardPerDevice(uint256 rewardPerDevice);
event ChangeRecipient(address indexed admin, uint256 projectId, address recipient);
event SetInvalidDevice(uint256 projectId, uint256 amount);

IioIDStore public ioIDStore;
uint256 public period;
uint256 public rewardPerDevice;
uint256 public projectNum;
mapping(uint256 => address) public projectRecipient;
mapping(uint256 => uint256) public projectInvalidDevice;
mapping(uint256 => uint256) public lastClaimedTimestamp;

function initialize(address _ioIDStore, uint256 _rewardPerDevice) public initializer {
require(_ioIDStore != address(0), "zero address");
require(_rewardPerDevice > 0, "invalid reward per device");

__Ownable_init_unchained();

ioIDStore = IioIDStore(_ioIDStore);
uint256 _period = 1 days;
period = _period;
rewardPerDevice = _rewardPerDevice;

emit Initialize(_period, _rewardPerDevice);
}

function addProject(uint256 _projectId, address _recipient, uint256 _startTimestamp) external onlyOwner {
require(_recipient != address(0), "zero address");
require(_startTimestamp > block.timestamp, "invalid start timestamp");
require(projectRecipient[_projectId] == address(0), "already added");
require(ioIDStore.projectDeviceContract(_projectId) != address(0), "invalid project");

projectNum++;
projectRecipient[_projectId] = _recipient;
lastClaimedTimestamp[_projectId] = _startTimestamp;
emit AddProject(_projectId, _recipient, _startTimestamp);
}

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

delete projectRecipient[_projectId];
delete lastClaimedTimestamp[_projectId];
projectNum--;
emit RemoveProject(_projectId);
}

function claim(uint256 _projectId) external returns (uint256) {
uint256 _lastClaimedTimestamp = lastClaimedTimestamp[_projectId];
require(_lastClaimedTimestamp != 0, "invalid project");
require(_lastClaimedTimestamp + period <= block.timestamp, "claim too short");
uint256 _claimablePeriods = (block.timestamp - _lastClaimedTimestamp) / period;
uint256 _rewards = _claimablePeriods *
rewardPerDevice *
(ioIDStore.projectActivedAmount(_projectId) - projectInvalidDevice[_projectId]);
require(address(this).balance >= _rewards, "insufficient fund");
lastClaimedTimestamp[_projectId] += (_claimablePeriods * period);
address _recipient = projectRecipient[_projectId];
(bool success, ) = payable(_recipient).call{value: _rewards}("");
require(success, "transfer rewards failed");
emit Claim(_projectId, _recipient, _lastClaimedTimestamp, lastClaimedTimestamp[_projectId], _rewards);
return _rewards;
}

function changePeriod(uint256 _period) external onlyOwner {
require(_period > 0, "invalid period");
period = _period;

emit ChangePeriod(_period);
}

function changeRewardPerDevice(uint256 _rewardPerDevice) external onlyOwner {
require(_rewardPerDevice > 0, "invalid reward per device");
rewardPerDevice = _rewardPerDevice;

emit ChangeRewardPerDevice(_rewardPerDevice);
}

function setInvalidDevice(uint256 _projectId, uint256 _amount) external onlyOwner {
require(ioIDStore.projectActivedAmount(_projectId) >= _amount, "invalid project");

projectInvalidDevice[_projectId] = _amount;
emit SetInvalidDevice(_projectId, _amount);
}

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);
}
}
15 changes: 15 additions & 0 deletions contracts/test/TestIoIDStore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TestIoIDStore {
mapping(uint256 => address) public projectDeviceContract;
mapping(uint256 => uint256) public projectActivedAmount;

function setProjectDeviceContract(uint256 _projectId, address _contract) external {
projectDeviceContract[_projectId] = _contract;
}

function setProjectActivedAmount(uint256 _projectId, uint256 _amount) external {
projectActivedAmount[_projectId] = _amount;
}
}
77 changes: 77 additions & 0 deletions test/TestPeriodClaimVault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect } from 'chai';
import { ethers } from 'hardhat';
import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers';

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

const ioIDStore = await ethers.deployContract('TestIoIDStore');
const vault = await ethers.deployContract('PeriodClaimVault');
await vault.connect(owner).initialize(ioIDStore.target, ethers.parseEther('0.1'));

expect(await vault.period()).to.equal(86400);
expect(await vault.rewardPerDevice()).to.equal(ethers.parseEther('0.1'));
await expect(vault.connect(fakeRecipient).changePeriod(10)).to.revertedWith('Ownable: caller is not the owner');
await expect(vault.connect(fakeRecipient).changeRewardPerDevice(10)).to.revertedWith(
'Ownable: caller is not the owner',
);
await expect(vault.connect(fakeRecipient).changeRecipient(1, fakeRecipient.address)).to.revertedWith(
'invalid admin',
);

await expect(vault.setInvalidDevice(1, 10)).to.revertedWith('invalid project');
expect(await vault.projectInvalidDevice(1)).to.be.equals(0);
await ioIDStore.setProjectActivedAmount(1, 5);
await ioIDStore.setProjectDeviceContract(1, '0x0000000000000000000000000000000000000001');
await expect(vault.setInvalidDevice(1, 10)).to.revertedWith('invalid project');
expect(await vault.projectInvalidDevice(1)).to.be.equals(0);
await vault.setInvalidDevice(1, 2);
expect(await vault.projectInvalidDevice(1)).to.be.equals(2);
await vault.setInvalidDevice(1, 0);
expect(await vault.projectInvalidDevice(1)).to.be.equals(0);

const latestBlock = await ethers.provider.getBlock('latest');
const startTimestamp = latestBlock!.timestamp + 2000;

const projectId = 1;
const projectRecipient = '0x0000000000000000000000000000000000000100';
await expect(vault.addProject(2, projectRecipient, startTimestamp)).to.revertedWith('invalid project');
await vault.addProject(projectId, projectRecipient, startTimestamp);
await expect(vault.addProject(projectId, projectRecipient, startTimestamp)).to.revertedWith('already added');

expect(await vault.lastClaimedTimestamp(projectId)).to.be.equals(startTimestamp);
await expect(vault.claim(projectId)).to.revertedWith('claim too short');
await time.increaseTo(startTimestamp + 86000);
await expect(vault.claim(projectId)).to.revertedWith('claim too short');
await time.increaseTo(startTimestamp + 86400);

await vault.donate({ value: ethers.parseEther('100') });
await vault.claim(projectId);
expect(await ethers.provider.getBalance(projectRecipient)).to.be.equals(
(await ioIDStore.projectActivedAmount(projectId)) * (await vault.rewardPerDevice()),
);
await expect(vault.claim(projectId)).to.revertedWith('claim too short');

await vault.setInvalidDevice(projectId, 2);
await expect(vault.claim(projectId)).to.revertedWith('claim too short');
await time.increaseTo(startTimestamp + 86400 * 2 + 5000);
let balanceRecipient = await ethers.provider.getBalance(projectRecipient);
await vault.claim(projectId);
expect((await ethers.provider.getBalance(projectRecipient)) - balanceRecipient).to.be.equals(
((await ioIDStore.projectActivedAmount(projectId)) - (await vault.projectInvalidDevice(projectId))) *
(await vault.rewardPerDevice()),
);

await time.increaseTo(startTimestamp + 86400 * 5 + 50000);
await vault.connect(fakeRecipient).claim(projectId);
expect((await ethers.provider.getBalance(projectRecipient)) - balanceRecipient).to.be.equals(
((await ioIDStore.projectActivedAmount(projectId)) - (await vault.projectInvalidDevice(projectId))) *
(await vault.rewardPerDevice()) *
BigInt(4),
);

await vault.removeProject(projectId);
await expect(vault.claim(projectId)).to.revertedWith('invalid project');
});
});

0 comments on commit ca52db4

Please sign in to comment.