// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
struct Rental {
address renter;
address lender;
uint256 pendingAmount;
uint256 totalAmount;
uint256 pendingDisputeAmount;
uint256 totalDisputeAmount;
uint256 startTime;
uint256 endTime;
bool ended;
bool active;
}
contract NodeTreasurer is ReentrancyGuard {
// The owner of the contract
address public owner;
// The system that will call the functions
address private system;
// The staking contract that implements a depositReward function
address public stakingContract;
// The rentals
mapping(bytes32 => Rental) public rentals;
// Whether new rentals are paused
bool public isPaused;
// The percentage of revenue to share
uint256 public revenueSharePercentage;
// The total revenue shared
uint256 public totalRevenueShared;
// Events
event Deposit(bytes32 key, uint256 amount);
event Withdraw(bytes32 key, uint256 amount);
event Dispute(bytes32 key, uint256 disputeAmount);
event Refund(bytes32 key, uint256 amount);
event Migrated(address indexed newTreasurer, uint256 ethAmount);
constructor() {
owner = msg.sender;
}
modifier onlySystem() {
require(
msg.sender == system || msg.sender == owner,
'Only system can call this function.'
);
_;
}
modifier onlyOwner() {
require(msg.sender == owner, 'Only owner can call this function.');
_;
}
/* Will be called by owner to set the system */
function setSystem(address _system) external onlyOwner {
system = _system;
}
/* Will be called by our systems to set the revenue share percentage */
function setRevenueSharePercentage(
uint256 _revenueSharePercentage
) external onlyOwner {
require(
_revenueSharePercentage <= 100,
'Revenue share percentage must be less than or equal to 100'
);
revenueSharePercentage = _revenueSharePercentage;
}
/* Will be called by our systems to set the staking contract */
function setStakingContract(address _stakingContract) external onlyOwner {
stakingContract = _stakingContract;
}
/* Will be called by our systems to pause new rentals */
function pause(bool state) external onlyOwner {
require(state != isPaused, 'State is already set to this value');
isPaused = state;
}
/* Will be called by the renter to deposit the rental amount */
function deposit(bytes32 key) external payable nonReentrant {
require(msg.value > 0, 'Deposit must be greater than 0');
require(!isPaused, 'Rentals are paused');
require(rentals[key].totalAmount == 0, 'Rental already exists');
Rental storage rental = rentals[key];
rental.renter = msg.sender;
rental.pendingAmount = msg.value;
rental.totalAmount = msg.value;
rental.pendingDisputeAmount = 0;
rental.totalDisputeAmount = 0;
rental.active = false;
emit Deposit(key, msg.value);
}
/* Will be called by the renter to withdraw the rental amount */
function withdraw(bytes32 key) external nonReentrant {
Rental storage rental = rentals[key];
require(block.timestamp >= rental.endTime, 'Rental period has not ended');
require(rental.active, 'Rental is not active');
require(!rental.ended, 'Rental has already ended');
require(msg.sender == rental.lender, 'Only lender can withdraw');
require(stakingContract != address(0), 'Staking contract is not set');
uint256 amountToPay = rental.totalAmount - rental.totalDisputeAmount;
rental.ended = true;
rental.pendingAmount = 0;
uint256 revenueShare = (amountToPay * revenueSharePercentage) / 100;
uint256 amountToWithdraw = amountToPay - revenueShare;
(bool sent, ) = stakingContract.call{value: revenueShare}('');
require(sent, 'Failed to send revenue share amount to staking contract');
totalRevenueShared += revenueShare;
(bool withdrawSent, ) = msg.sender.call{value: amountToWithdraw}('');
require(withdrawSent, 'Failed to send amount to withdraw');
emit Withdraw(key, amountToWithdraw);
}
/* Will be called by the lender to claim the refund */
function claimRefund(bytes32 key) external nonReentrant {
Rental storage rental = rentals[key];
require(msg.sender == rental.renter, 'Only renter can claim refund');
require(rental.pendingDisputeAmount > 0, 'No dispute amount to refund');
(bool disputeSent, ) = msg.sender.call{value: rental.pendingDisputeAmount}(
''
);
require(disputeSent, 'Failed to send dispute amount');
emit Refund(key, rental.pendingDisputeAmount);
rental.pendingDisputeAmount = 0;
}
/* Will be set from our systems and starts the rental period */
function setRentalInfo(
bytes32 key,
address lender,
uint256 endTime
) external onlySystem {
Rental storage rental = rentals[key];
rental.lender = lender;
rental.startTime = block.timestamp;
rental.endTime = endTime;
rental.active = true;
}
/* Will be set from our systems that detects uptime */
function raiseDispute(
bytes32 key,
uint256 disputeAmount
) external onlySystem {
Rental storage rental = rentals[key];
require(rental.active, 'Rental is not active');
require(disputeAmount > 0, 'Dispute amount must be greater than 0');
require(
disputeAmount <= rental.totalAmount - rental.totalDisputeAmount,
'Dispute amount exceeds rental amount'
);
rental.pendingDisputeAmount += disputeAmount;
rental.totalDisputeAmount += disputeAmount;
emit Dispute(key, disputeAmount);
}
/* Migrates the treasurer contract to a new contract */
function migrate(address _newStakingContract) external onlyOwner {
require(_newStakingContract != address(0), 'Invalid address');
uint256 contractETHBalance = address(this).balance;
(bool sent, ) = _newStakingContract.call{value: contractETHBalance}('');
require(sent, 'Failed to transfer ETH');
emit Migrated(
_newStakingContract,
contractETHBalance
);
}
receive() external payable {}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/ReentrancyGuard.sol)
pragma solidity ^0.8.20;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status;
/**
* @dev Unauthorized reentrant call.
*/
error ReentrancyGuardReentrantCall();
constructor() {
_status = NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be NOT_ENTERED
if (_status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
_status = ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == ENTERED;
}
}
{
"compilationTarget": {
"contracts/NodeTreasurer.sol": "NodeTreasurer"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": false,
"runs": 200
},
"remappings": []
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ReentrancyGuardReentrantCall","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"key","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"key","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"disputeAmount","type":"uint256"}],"name":"Dispute","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newTreasurer","type":"address"},{"indexed":false,"internalType":"uint256","name":"ethAmount","type":"uint256"}],"name":"Migrated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"key","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Refund","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"key","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[{"internalType":"bytes32","name":"key","type":"bytes32"}],"name":"claimRefund","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"key","type":"bytes32"}],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newStakingContract","type":"address"}],"name":"migrate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"state","type":"bool"}],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"key","type":"bytes32"},{"internalType":"uint256","name":"disputeAmount","type":"uint256"}],"name":"raiseDispute","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"rentals","outputs":[{"internalType":"address","name":"renter","type":"address"},{"internalType":"address","name":"lender","type":"address"},{"internalType":"uint256","name":"pendingAmount","type":"uint256"},{"internalType":"uint256","name":"totalAmount","type":"uint256"},{"internalType":"uint256","name":"pendingDisputeAmount","type":"uint256"},{"internalType":"uint256","name":"totalDisputeAmount","type":"uint256"},{"internalType":"uint256","name":"startTime","type":"uint256"},{"internalType":"uint256","name":"endTime","type":"uint256"},{"internalType":"bool","name":"ended","type":"bool"},{"internalType":"bool","name":"active","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"revenueSharePercentage","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"key","type":"bytes32"},{"internalType":"address","name":"lender","type":"address"},{"internalType":"uint256","name":"endTime","type":"uint256"}],"name":"setRentalInfo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_revenueSharePercentage","type":"uint256"}],"name":"setRevenueSharePercentage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_stakingContract","type":"address"}],"name":"setStakingContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_system","type":"address"}],"name":"setSystem","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stakingContract","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalRevenueShared","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"key","type":"bytes32"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]