-
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 #21 from iotexproject/feat/time_batch_claim_vault
add PeriodClaimVault
- Loading branch information
Showing
4 changed files
with
219 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
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,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); | ||
} | ||
} |
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,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; | ||
} | ||
} |
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,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'); | ||
}); | ||
}); |