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

Audited delegation feature #44

Merged
merged 17 commits into from
Jan 6, 2025
109 changes: 109 additions & 0 deletions smart-contracts/contracts/delegate/DelegateFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";

import {IProvidersDelegate} from "../interfaces/delegate/IProvidersDelegate.sol";
import {IDelegateFactory} from "../interfaces/delegate/IDelegateFactory.sol";
import {IOwnable} from "../interfaces/utils/IOwnable.sol";

contract DelegateFactory is IDelegateFactory, OwnableUpgradeable, PausableUpgradeable, UUPSUpgradeable {
address public lumerinDiamond;
address public beacon;

mapping(address => address[]) public proxies;
uint128 public minDeregistrationTimeout;

constructor() {
_disableInitializers();
}

function DelegateFactory_init(
address lumerinDiamond_,
address implementation_,
uint128 minDeregistrationTimeout_
) external initializer {
__Pausable_init();
__Ownable_init();
__UUPSUpgradeable_init();

setMinDeregistrationTimeout(minDeregistrationTimeout_);
lumerinDiamond = lumerinDiamond_;

beacon = address(new UpgradeableBeacon(implementation_));
}

function pause() external onlyOwner {
_pause();
}

function unpause() external onlyOwner {
_unpause();
}

function setMinDeregistrationTimeout(uint128 minDeregistrationTimeout_) public onlyOwner {
minDeregistrationTimeout = minDeregistrationTimeout_;

emit MinDeregistrationTimeoutUpdated(minDeregistrationTimeout_);
}

function deployProxy(
address feeTreasury_,
uint256 fee_,
string memory name_,
string memory endpoint_,
uint128 deregistrationOpensAt_
) external whenNotPaused returns (address) {
if (deregistrationOpensAt_ <= block.timestamp + minDeregistrationTimeout) {
revert InvalidDeregistrationOpenAt(deregistrationOpensAt_, uint128(block.timestamp + minDeregistrationTimeout));
}

bytes32 salt_ = _calculatePoolSalt(_msgSender());
address proxy_ = address(new BeaconProxy{salt: salt_}(beacon, bytes("")));

proxies[_msgSender()].push(proxy_);

IProvidersDelegate(proxy_).ProvidersDelegate_init(
lumerinDiamond,
feeTreasury_,
fee_,
name_,
endpoint_,
deregistrationOpensAt_
);
IOwnable(proxy_).transferOwnership(_msgSender());

emit ProxyDeployed(proxy_);

return proxy_;
}

function predictProxyAddress(address deployer_) external view returns (address) {
bytes32 salt_ = _calculatePoolSalt(deployer_);

bytes32 bytecodeHash_ = keccak256(
abi.encodePacked(type(BeaconProxy).creationCode, abi.encode(address(beacon), bytes("")))
);

return Create2.computeAddress(salt_, bytecodeHash_);
}

function updateImplementation(address newImplementation_) external onlyOwner {
UpgradeableBeacon(beacon).upgradeTo(newImplementation_);
}

function version() external pure returns (uint256) {
return 1;
}

function _calculatePoolSalt(address sender_) internal view returns (bytes32) {
return keccak256(abi.encodePacked(sender_, proxies[sender_].length));
}

function _authorizeUpgrade(address) internal view override onlyOwner {}
}
275 changes: 275 additions & 0 deletions smart-contracts/contracts/delegate/ProvidersDelegate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

import {PRECISION} from "@solarity/solidity-lib/utils/Globals.sol";

import {IProvidersDelegate} from "../interfaces/delegate/IProvidersDelegate.sol";
import {IBidStorage} from "../interfaces/storage/IBidStorage.sol";
import {ISessionRouter} from "../interfaces/facets/ISessionRouter.sol";
import {IProviderRegistry} from "../interfaces/facets/IProviderRegistry.sol";
import {IMarketplace} from "../interfaces/facets/IMarketplace.sol";

contract ProvidersDelegate is IProvidersDelegate, OwnableUpgradeable {
using SafeERC20 for IERC20;
using Math for uint256;

// The contract deps
address public lumerinDiamond;
address public token;

// The owner fee
address public feeTreasury;
uint256 public fee;

// The contract metadata
string public name;
string public endpoint;

// The main calculation storage
uint256 public totalStaked;
uint256 public totalRate;
uint256 public lastContractBalance;

// The Staker data
bool public isStakeClosed;
mapping(address => Staker) public stakers;

// Deregistration limits
uint128 public deregistrationOpensAt;

constructor() {
_disableInitializers();
}

function ProvidersDelegate_init(
address lumerinDiamond_,
address feeTreasury_,
uint256 fee_,
string memory name_,
string memory endpoint_,
uint128 deregistrationOpensAt_
) external initializer {
__Ownable_init();

lumerinDiamond = lumerinDiamond_;
token = IBidStorage(lumerinDiamond).getToken();

setName(name_);
setEndpoint(endpoint_);
setFeeTreasury(feeTreasury_);

if (fee_ > PRECISION) {
revert InvalidFee(fee_, PRECISION);
}

fee = fee_;
deregistrationOpensAt = deregistrationOpensAt_;

IERC20(token).approve(lumerinDiamond_, type(uint256).max);
}

function setName(string memory name_) public onlyOwner {
if (bytes(name_).length == 0) {
revert InvalidNameLength();
}

name = name_;

emit NameUpdated(name_);
}

function setEndpoint(string memory endpoint_) public onlyOwner {
if (bytes(endpoint_).length == 0) {
revert InvalidEndpointLength();
}

endpoint = endpoint_;

emit EndpointUpdated(endpoint_);
}

function setFeeTreasury(address feeTreasury_) public onlyOwner {
if (feeTreasury_ == address(0)) {
revert InvalidFeeTreasuryAddress();
}

feeTreasury = feeTreasury_;

emit FeeTreasuryUpdated(feeTreasury_);
}

function setIsStakeClosed(bool isStakeClosed_) public onlyOwner {
isStakeClosed = isStakeClosed_;

emit IsStakeClosedUpdated(isStakeClosed_);
}

function setIsRestakeDisabled(bool isRestakeDisabled_) external {
stakers[_msgSender()].isRestakeDisabled = isRestakeDisabled_;

emit IsRestakeDisabledUpdated(_msgSender(), isRestakeDisabled_);
}

function stake(uint256 amount_) external {
_stake(_msgSender(), amount_);
}

function _stake(address staker_, uint256 amount_) private {
if (isStakeClosed && !isStakeAfterDeregisterAvailable()) {
revert StakeClosed();
}
if (amount_ == 0) {
revert InsufficientAmount();
}

Staker storage staker = stakers[staker_];

(uint256 currentRate_, uint256 contractBalance_) = getCurrentRate();
uint256 pendingRewards_ = _getCurrentStakerRewards(currentRate_, staker);

IERC20(token).safeTransferFrom(staker_, address(this), amount_);

totalRate = currentRate_;
totalStaked += amount_;

lastContractBalance = contractBalance_;

staker.rate = currentRate_;
staker.staked += amount_;
staker.pendingRewards = pendingRewards_;

IProviderRegistry(lumerinDiamond).providerRegister(address(this), amount_, endpoint);

emit Staked(staker_, staker.staked, totalStaked, staker.rate);
}

function restake(address staker_, uint256 amount_) external {
if (_msgSender() != staker_ && _msgSender() != owner()) {
revert RestakeInvalidCaller(_msgSender(), staker_);
}
if (_msgSender() == owner() && stakers[staker_].isRestakeDisabled) {
revert RestakeDisabled(staker_);
}

amount_ = claim(staker_, amount_);
_stake(staker_, amount_);
}

function claim(address staker_, uint256 amount_) public returns (uint256) {
Staker storage staker = stakers[staker_];

(uint256 currentRate_, uint256 contractBalance_) = getCurrentRate();
uint256 pendingRewards_ = _getCurrentStakerRewards(currentRate_, staker);

amount_ = amount_.min(contractBalance_).min(pendingRewards_);
if (amount_ == 0) {
revert ClaimAmountIsZero();
}

totalRate = currentRate_;

lastContractBalance = contractBalance_ - amount_;

staker.rate = currentRate_;
staker.pendingRewards = pendingRewards_ - amount_;
staker.claimed += amount_;

uint256 feeAmount_ = (amount_ * fee) / PRECISION;
if (feeAmount_ != 0) {
IERC20(token).safeTransfer(feeTreasury, feeAmount_);

amount_ -= feeAmount_;

emit FeeClaimed(feeTreasury, feeAmount_);
}

IERC20(token).safeTransfer(staker_, amount_);

emit Claimed(staker_, staker.claimed, staker.rate);

return amount_;
}

function getCurrentRate() public view returns (uint256, uint256) {
uint256 contractBalance_ = IERC20(token).balanceOf(address(this));

if (totalStaked == 0) {
return (totalRate, contractBalance_);
}

uint256 reward_ = contractBalance_ - lastContractBalance;
uint256 rate_ = totalRate + (reward_ * PRECISION) / totalStaked;

return (rate_, contractBalance_);
}

function getCurrentStakerRewards(address staker_) public view returns (uint256) {
(uint256 currentRate_, ) = getCurrentRate();

return _getCurrentStakerRewards(currentRate_, stakers[staker_]);
}

function providerDeregister(bytes32[] calldata bidIds_) external {
if (!isDeregisterAvailable()) {
_checkOwner();
}

_deleteModelBids(bidIds_);
IProviderRegistry(lumerinDiamond).providerDeregister(address(this));

fee = 0;
}

function postModelBid(bytes32 modelId_, uint256 pricePerSecond_) external onlyOwner returns (bytes32) {
if (isDeregisterAvailable()) {
revert BidCannotBeCreatedDuringThisPeriod();
}

IERC20(token).safeTransferFrom(_msgSender(), address(this), IMarketplace(lumerinDiamond).getBidFee());

return IMarketplace(lumerinDiamond).postModelBid(address(this), modelId_, pricePerSecond_);
}

function deleteModelBids(bytes32[] calldata bidIds_) external {
if (!isDeregisterAvailable()) {
_checkOwner();
}

_deleteModelBids(bidIds_);
}

function claimForProvider(bytes32 sessionId_) external {
ISessionRouter(lumerinDiamond).claimForProvider(sessionId_);
}

function isDeregisterAvailable() public view returns (bool) {
return block.timestamp >= deregistrationOpensAt;
}

function isStakeAfterDeregisterAvailable() public view returns (bool) {
IProviderRegistry.Provider memory provider_ = IProviderRegistry(lumerinDiamond).getProvider(address(this));
return isDeregisterAvailable() && provider_.stake > 0 && provider_.isDeleted;
}

function _deleteModelBids(bytes32[] calldata bidIds_) private {
address lumerinDiamond_ = lumerinDiamond;

for (uint256 i = 0; i < bidIds_.length; i++) {
IMarketplace(lumerinDiamond_).deleteModelBid(bidIds_[i]);
}
}

function _getCurrentStakerRewards(uint256 delegatorRate_, Staker memory staker_) private pure returns (uint256) {
uint256 newRewards_ = ((delegatorRate_ - staker_.rate) * staker_.staked) / PRECISION;

return staker_.pendingRewards + newRewards_;
}

function version() external pure returns (uint256) {
return 1;
}
}
Loading