Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add PeriodClaimVault #21

Merged
merged 3 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
Loading