diff --git a/smart-contracts/contracts/delegate/DelegateFactory.sol b/smart-contracts/contracts/delegate/DelegateFactory.sol new file mode 100644 index 00000000..8363b306 --- /dev/null +++ b/smart-contracts/contracts/delegate/DelegateFactory.sol @@ -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 {} +} diff --git a/smart-contracts/contracts/delegate/ProvidersDelegate.sol b/smart-contracts/contracts/delegate/ProvidersDelegate.sol new file mode 100644 index 00000000..4f51e729 --- /dev/null +++ b/smart-contracts/contracts/delegate/ProvidersDelegate.sol @@ -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; + } +} diff --git a/smart-contracts/contracts/diamond/facets/SessionRouter.sol b/smart-contracts/contracts/diamond/facets/SessionRouter.sol index 5b330636..53e18b79 100644 --- a/smart-contracts/contracts/diamond/facets/SessionRouter.sol +++ b/smart-contracts/contracts/diamond/facets/SessionRouter.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.24; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -32,6 +34,7 @@ contract SessionRouter is using LibSD for LibSD.SD; using SafeERC20 for IERC20; using EnumerableSet for EnumerableSet.Bytes32Set; + using Address for address; function __SessionRouter_init( address fundingAccount_, @@ -187,19 +190,16 @@ contract SessionRouter is } function _extractProviderApproval(bytes calldata providerApproval_) private view returns (bytes32) { - (bytes32 bidId_, uint256 chainId_, address user_, uint128 timestamp_) = abi.decode( + (bytes32 bidId_, uint256 chainId_, , uint128 timestamp_) = abi.decode( providerApproval_, (bytes32, uint256, address, uint128) ); - if (user_ != _msgSender()) { - revert SessionApprovedForAnotherUser(); - } if (chainId_ != block.chainid) { - revert SesssionApprovedForAnotherChainId(); + revert SessionApprovedForAnotherChainId(); } if (block.timestamp > timestamp_ + SIGNATURE_TTL) { - revert SesssionApproveExpired(); + revert SessionApproveExpired(); } return bidId_; @@ -240,10 +240,10 @@ contract SessionRouter is ); if (chainId_ != block.chainid) { - revert SesssionReceiptForAnotherChainId(); + revert SessionReceiptForAnotherChainId(); } if (block.timestamp > timestamp_ + SIGNATURE_TTL) { - revert SesssionReceiptExpired(); + revert SessionReceiptExpired(); } return (sessionId_, tpsScaled1000_, ttftMs_); @@ -533,9 +533,24 @@ contract SessionRouter is address provider_, bytes calldata receipt_, bytes calldata signature_ - ) private pure returns (bool) { + ) private view returns (bool) { bytes32 receiptHash_ = ECDSA.toEthSignedMessageHash(keccak256(receipt_)); + address user_ = ECDSA.recover(receiptHash_, signature_); + + if (user_ == provider_ || isRightsDelegated(user_, provider_, DELEGATION_RULES_PROVIDER)) { + return true; + } + + if (provider_.isContract()) { + (bool success, bytes memory result) = provider_.staticcall(abi.encodeWithSignature("owner()")); + if (success && result.length == 32) { + address owner_ = abi.decode(result, (address)); + if (user_ == owner_) { + return true; + } + } + } - return ECDSA.recover(receiptHash_, signature_) == provider_; + return false; } } diff --git a/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol b/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol new file mode 100644 index 00000000..cd749e71 --- /dev/null +++ b/smart-contracts/contracts/interfaces/delegate/IDelegateFactory.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IDelegateFactory { + error InvalidDeregistrationOpenAt(uint128 value, uint128 minimal); + + /** + * The event that is emitted when the proxy deployed. + * @param proxyAddress The pool's id. + */ + event ProxyDeployed(address indexed proxyAddress); + + /** + * The event that is emitted when the `minDeregistrationTimeout` changed. + * @param minDeregistrationTimeout_ The pool's id. + */ + event MinDeregistrationTimeoutUpdated(uint128 minDeregistrationTimeout_); + + /** + * The function to initialize the contract. + * @param lumerinDiamond_ The Lumerin protocol address. + * @param implementation_ The implementation address. + * @param minDeregistrationTimeout_ The minimal timestamp before deregistration will start + */ + function DelegateFactory_init(address lumerinDiamond_, address implementation_, uint128 minDeregistrationTimeout_) external; + + /** + * Triggers stopped state. + */ + function pause() external; + + /** + * Returns to normal state. + */ + function unpause() external; + + /** + * The function to deploy the new proxy contract. + * @param feeTreasury_ The subnet fee treasury. + * @param fee_ The fee percent where 100% = 10^25. + * @param name_ The Subnet name. + * @param endpoint_ The subnet endpoint. + * @param deregistrationOpensAt_ Provider deregistration will be available after this timestamp. + * @return Deployed proxy address + */ + function deployProxy( + address feeTreasury_, + uint256 fee_, + string memory name_, + string memory endpoint_, + uint128 deregistrationOpensAt_ + ) external returns (address); + + /** + * The function to predict new proxy address. + * @param deployer_ The deployer address. + */ + function predictProxyAddress(address deployer_) external view returns (address); + + /** + * The function to upgrade the implementation. + * @param newImplementation_ The new implementation address. + */ + function updateImplementation(address newImplementation_) external; + + /** + * @return The contract version. + */ + function version() external pure returns (uint256); +} diff --git a/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol new file mode 100644 index 00000000..b0975c63 --- /dev/null +++ b/smart-contracts/contracts/interfaces/delegate/IProvidersDelegate.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IProvidersDelegate { + error InvalidNameLength(); + error InvalidEndpointLength(); + error InvalidFeeTreasuryAddress(); + error InvalidFee(uint256 current, uint256 max); + error StakeClosed(); + error ProviderDeregistered(); + error BidCannotBeCreatedDuringThisPeriod(); + error InsufficientAmount(); + error RestakeDisabled(address staker); + error RestakeInvalidCaller(address caller, address staker); + error ClaimAmountIsZero(); + + /** + * The event that is emitted when the name updated. + * @param name The new value. + */ + event NameUpdated(string name); + + /** + * The event that is emitted when the endpoint updated. + * @param endpoint The new value. + */ + event EndpointUpdated(string endpoint); + + /** + * The event that is emitted when the `feeTreasury` updated. + * @param feeTreasury The new value. + */ + event FeeTreasuryUpdated(address feeTreasury); + + /** + * The event that is emitted when the stake closed or opened for all users. + * @param isStakeClosed The new value. + */ + event IsStakeClosedUpdated(bool isStakeClosed); + + /** + * The event that is emitted when restake was disabled or enabled by user. + * @param staker The user address. + * @param isRestakeDisabled The new value. + */ + event IsRestakeDisabledUpdated(address staker, bool isRestakeDisabled); + + /** + * The event that is emitted when user staked. + * @param staker The user address. + * @param staked The total staked amount for user. + * @param totalStaked The total staked amount for the contract. + * @param rate The contract rate. + */ + event Staked(address staker, uint256 staked, uint256 totalStaked, uint256 rate); + + /** + * The event that is emitted when user claimed. + * @param staker The user address. + * @param claimed The total claimed amount for user. + * @param rate The contract rate. + */ + event Claimed(address staker, uint256 claimed, uint256 rate); + + /** + * The event that is emitted when rewards claimed for owner. + * @param feeTreasury The fee treasury address. + * @param feeAmount The fee amount. + */ + event FeeClaimed(address feeTreasury, uint256 feeAmount); + + + + /** + * @param staked Staked amount. + * @param claimed Claimed amount. + * @param rate The user internal rate. + * @param pendingRewards Pending rewards for claim. + * @param isRestakeDisabled If true, restake isn't available. + */ + struct Staker { + uint256 staked; + uint256 claimed; + uint256 rate; + uint256 pendingRewards; + bool isRestakeDisabled; + } + + /** + * The function to initialize the contract. + * @param lumerinDiamond_ The Lumerin protocol address. + * @param feeTreasury_ The subnet fee treasury. + * @param fee_ The fee percent where 100% = 10^25. + * @param name_ The Subnet name. + * @param endpoint_ The subnet endpoint. + * @param deregistrationOpensAt_ Provider deregistration will be available after this time. + */ + function ProvidersDelegate_init( + address lumerinDiamond_, + address feeTreasury_, + uint256 fee_, + string memory name_, + string memory endpoint_, + uint128 deregistrationOpensAt_ + ) external; + + /** + * The function to set the Subnet name. + * @param name_ New name. + */ + function setName(string memory name_) external; + + /** + * The function to set the new endpoint. + * @param endpoint_ New endpoint. + */ + function setEndpoint(string memory endpoint_) external; + + /** + * The function to set fee treasury address. + * @param feeTreasury_ New address + */ + function setFeeTreasury(address feeTreasury_) external; + + /** + * The function close or open possibility to stake new tokens. + * @param isStakeClosed_ True or False. + */ + function setIsStakeClosed(bool isStakeClosed_) external; + + /** + * The function to disabled possibility for restake. + * @param isRestakeDisabled_ True or False. + */ + function setIsRestakeDisabled(bool isRestakeDisabled_) external; + + /** + * The function to stake tokens. + * @param amount_ Amount to stake. + */ + function stake(uint256 amount_) external; + + /** + * The function to restake rewards. + * @param staker_ Staker address. + * @param amount_ Amount to stake. + */ + function restake(address staker_, uint256 amount_) external; + + /** + * The function to claim rewards. + * @param staker_ Staker address. + * @param amount_ Amount to stake. + */ + function claim(address staker_, uint256 amount_) external returns(uint256); + + /** + * The function to return the current rate. + */ + function getCurrentRate() external view returns (uint256, uint256); + + /** + * The function to get amount of the Staker rewards. + * @param staker_ Staker address. + */ + function getCurrentStakerRewards(address staker_) external view returns (uint256); + + /** + * The function to deregister the provider. + * @param bidIds_ Bid IDs. + */ + function providerDeregister(bytes32[] calldata bidIds_) external; + + /** + * The function create the model bid. + * @param modelId_ Model ID. + * @param pricePerSecond_ Price per second. + */ + function postModelBid(bytes32 modelId_, uint256 pricePerSecond_) external returns (bytes32); + + /** + * The function to delete model bids. + * @param bidIds_ Bid IDs. + */ + function deleteModelBids(bytes32[] calldata bidIds_) external; + + /** + * The function to get contract version. + */ + function version() external pure returns (uint256); +} diff --git a/smart-contracts/contracts/interfaces/facets/ISessionRouter.sol b/smart-contracts/contracts/interfaces/facets/ISessionRouter.sol index ff759435..3fe220ce 100644 --- a/smart-contracts/contracts/interfaces/facets/ISessionRouter.sol +++ b/smart-contracts/contracts/interfaces/facets/ISessionRouter.sol @@ -10,12 +10,12 @@ interface ISessionRouter is ISessionStorage { event SessionClosed(address indexed user, bytes32 indexed sessionId, address indexed providerId); event UserWithdrawn(address indexed user, uint256 amount_); error SessionProviderSignatureMismatch(); - error SesssionApproveExpired(); - error SesssionApprovedForAnotherChainId(); + error SessionApproveExpired(); + error SessionApprovedForAnotherChainId(); error SessionDuplicateApproval(); error SessionApprovedForAnotherUser(); - error SesssionReceiptForAnotherChainId(); - error SesssionReceiptExpired(); + error SessionReceiptForAnotherChainId(); + error SessionReceiptExpired(); error SessionTooShort(); error SessionAlreadyClosed(); error SessionNotEndedOrNotExist(); diff --git a/smart-contracts/contracts/interfaces/utils/IOwnable.sol b/smart-contracts/contracts/interfaces/utils/IOwnable.sol new file mode 100644 index 00000000..0d1aabc9 --- /dev/null +++ b/smart-contracts/contracts/interfaces/utils/IOwnable.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IOwnable { + /** + * @dev Returns the address of the current owner. + */ + function owner() external view returns (address); + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner_) external; +} diff --git a/smart-contracts/contracts/mock/UUPSMock.sol b/smart-contracts/contracts/mock/UUPSMock.sol new file mode 100644 index 00000000..585fa08b --- /dev/null +++ b/smart-contracts/contracts/mock/UUPSMock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +contract UUPSMock is UUPSUpgradeable { + function version() external pure returns (uint256) { + return 999; + } + + function _authorizeUpgrade(address) internal view override {} +} diff --git a/smart-contracts/contracts/mock/tokens/MorpheusToken.sol b/smart-contracts/contracts/mock/tokens/MorpheusToken.sol index fe5b5110..927524fc 100644 --- a/smart-contracts/contracts/mock/tokens/MorpheusToken.sol +++ b/smart-contracts/contracts/mock/tokens/MorpheusToken.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.24; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + contract MorpheusToken is ERC20 { // set the initial supply to 42 million like in whitepaper uint256 public constant INITIAL_SUPPLUY = 42_000_000 ether; diff --git a/smart-contracts/deploy/2_change_bid_price.migration.ts b/smart-contracts/deploy/2_change_bid_price.migration.ts index 98ad21c9..88ae3bf4 100644 --- a/smart-contracts/deploy/2_change_bid_price.migration.ts +++ b/smart-contracts/deploy/2_change_bid_price.migration.ts @@ -1,10 +1,13 @@ import { Deployer } from '@solarity/hardhat-migrate'; +import { parseConfig } from './helpers/config-parser'; + import { Marketplace__factory } from '@/generated-types/ethers'; module.exports = async function (deployer: Deployer) { - // const marketplaceFacet = await deployer.deployed(Marketplace__factory, '0xb8C55cD613af947E73E262F0d3C54b7211Af16CF'); - const marketplaceFacet = await deployer.deployed(Marketplace__factory, '0xDE819AaEE474626E3f34Ef0263373357e5a6C71b'); + const config = parseConfig(); + + const marketplaceFacet = await deployer.deployed(Marketplace__factory, config.lumerinProtocol); console.log(await marketplaceFacet.getMinMaxBidPricePerSecond()); diff --git a/smart-contracts/deploy/3_delegate_protocol.migration copy.ts b/smart-contracts/deploy/3_delegate_protocol.migration copy.ts new file mode 100644 index 00000000..38bdb0cb --- /dev/null +++ b/smart-contracts/deploy/3_delegate_protocol.migration copy.ts @@ -0,0 +1,38 @@ +import { Deployer } from '@solarity/hardhat-migrate'; + +import { parseConfig } from './helpers/config-parser'; + +import { + DelegateFactory__factory, + ERC1967Proxy__factory, + ProvidersDelegate__factory, +} from '@/generated-types/ethers'; +import { wei } from '@/scripts/utils/utils'; + +module.exports = async function (deployer: Deployer) { + const config = parseConfig(); + + const providersDelegatorImpl = await deployer.deploy(ProvidersDelegate__factory); + const delegatorFactoryImpl = await deployer.deploy(DelegateFactory__factory); + const proxy = await deployer.deploy(ERC1967Proxy__factory, [await delegatorFactoryImpl.getAddress(), '0x']); + + const delegatorFactory = await deployer.deployed(DelegateFactory__factory, await proxy.getAddress()); + + await delegatorFactory.DelegateFactory_init(config.lumerinProtocol, providersDelegatorImpl, 60 * 60); + + await delegatorFactory.deployProxy( + '0x19ec1E4b714990620edf41fE28e9a1552953a7F4', + wei(0.2, 25), + 'First Subnet', + 'Custom endpoint', + Math.floor((Date.now() / 1000)) + 24 * 60 * 60, + ); +}; + +// npx hardhat migrate --only 3 + +// npx hardhat migrate --network arbitrum_sepolia --only 3 --verify +// npx hardhat migrate --network arbitrum_sepolia --only 3 --verify --continue + +// npx hardhat migrate --network arbitrum --only 3 --verify +// npx hardhat migrate --network arbitrum --only 3 --verify --continue diff --git a/smart-contracts/deploy/4_update_session_facet.migration.ts b/smart-contracts/deploy/4_update_session_facet.migration.ts new file mode 100644 index 00000000..3b638f1c --- /dev/null +++ b/smart-contracts/deploy/4_update_session_facet.migration.ts @@ -0,0 +1,65 @@ +import { Deployer } from '@solarity/hardhat-migrate'; +import { Fragment } from 'ethers'; +import { ethers } from 'hardhat'; + +import { parseConfig } from './helpers/config-parser'; + +import { + ISessionRouter__factory, + IStatsStorage__factory, + LinearDistributionIntervalDecrease__factory, + LumerinDiamond__factory, + SessionRouter__factory, +} from '@/generated-types/ethers'; +import { FacetAction } from '@/test/helpers/deployers'; + +module.exports = async function (deployer: Deployer) { + const config = parseConfig(); + + const ldid = await deployer.deploy(LinearDistributionIntervalDecrease__factory); + const newSessionRouterFacet = await deployer.deploy(SessionRouter__factory, { + libraries: { + LinearDistributionIntervalDecrease: ldid, + }, + }); + + const lumerinDiamond = await deployer.deployed(LumerinDiamond__factory, config.lumerinProtocol); + + // ONLY FOR TESTS + // const testSigner = await ethers.getImpersonatedSigner(await lumerinDiamond.owner()); + // END + + const oldSessionRouterFacet = '0xCc48cB2DbA21A5D36C16f6f64e5B5E138EA1ba13'; + const oldSelectors = await lumerinDiamond.facetFunctionSelectors(oldSessionRouterFacet); + + // ONLY FOR TESTS - remove or add `.connect(testSigner)` + await lumerinDiamond['diamondCut((address,uint8,bytes4[])[])']([ + { + facetAddress: oldSessionRouterFacet, + action: FacetAction.Remove, + functionSelectors: [...oldSelectors], + }, + { + facetAddress: newSessionRouterFacet, + action: FacetAction.Add, + functionSelectors: ISessionRouter__factory.createInterface() + .fragments.filter(Fragment.isFunction) + .map((f) => f.selector), + }, + { + facetAddress: newSessionRouterFacet, + action: FacetAction.Add, + functionSelectors: IStatsStorage__factory.createInterface() + .fragments.filter(Fragment.isFunction) + .map((f) => f.selector), + }, + ]); +}; + +// npx hardhat migrate --only 4 + +// npx hardhat migrate --network arbitrum_sepolia --only 4 --verify +// npx hardhat migrate --network arbitrum_sepolia --only 4 --verify --continue + +// npx hardhat migrate --network arbitrum --only 4 --verify +// npx hardhat migrate --network arbitrum --only 4 --verify --continue diff --git a/smart-contracts/deploy/data/config_arbitrum_mainnet.json b/smart-contracts/deploy/data/config_arbitrum_mainnet.json index f20a0c6a..6eaca754 100644 --- a/smart-contracts/deploy/data/config_arbitrum_mainnet.json +++ b/smart-contracts/deploy/data/config_arbitrum_mainnet.json @@ -8,6 +8,7 @@ "marketplaceMaxBidPricePerSecond": "10000000000000000", "delegateRegistry": "0x00000000000000447e69651d841bD8D104Bed493", "owner": "0x1FE04BC15Cf2c5A2d41a0b3a96725596676eBa1E", + "lumerinProtocol": "0xDE819AaEE474626E3f34Ef0263373357e5a6C71b", "pools": [ { "payoutStart": 1707393600, diff --git a/smart-contracts/deploy/data/config_arbitrum_sepolia.json b/smart-contracts/deploy/data/config_arbitrum_sepolia.json index 097fe54d..97c5be2f 100644 --- a/smart-contracts/deploy/data/config_arbitrum_sepolia.json +++ b/smart-contracts/deploy/data/config_arbitrum_sepolia.json @@ -8,6 +8,7 @@ "marketplaceMaxBidPricePerSecond": "20000000000000000000", "delegateRegistry": "0x00000000000000447e69651d841bD8D104Bed493", "owner": "0x1FE04BC15Cf2c5A2d41a0b3a96725596676eBa1E", + "lumerinProtocol": "0xb8C55cD613af947E73E262F0d3C54b7211Af16CF", "pools": [ { "payoutStart": 1707393600, diff --git a/smart-contracts/deploy/helpers/config-parser.ts b/smart-contracts/deploy/helpers/config-parser.ts index 00c27417..7bd950cd 100644 --- a/smart-contracts/deploy/helpers/config-parser.ts +++ b/smart-contracts/deploy/helpers/config-parser.ts @@ -13,10 +13,11 @@ export type Config = { marketplaceMaxBidPricePerSecond: string; delegateRegistry: string; owner: string; + lumerinProtocol: string; }; export function parseConfig(): Config { - const configPath = `deploy/data/config_arbitrum_mainnet.json`; + const configPath = `deploy/data/config_arbitrum_sepolia.json`; return JSON.parse(readFileSync(configPath, 'utf-8')) as Config; } diff --git a/smart-contracts/hardhat.config.ts b/smart-contracts/hardhat.config.ts index 71780988..7f6235f7 100644 --- a/smart-contracts/hardhat.config.ts +++ b/smart-contracts/hardhat.config.ts @@ -39,9 +39,12 @@ const config: HardhatUserConfig = { // forking: { // url: `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_KEY}`, // }, - forking: { - url: `https://arbitrum-mainnet.infura.io/v3/${process.env.INFURA_KEY}`, - }, + // forking: { + // url: `https://arb-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, + // }, + // forking: { + // url: `https://arbitrum-mainnet.infura.io/v3/${process.env.INFURA_KEY}`, + // }, }, localhost: { url: 'http://127.0.0.1:8545', diff --git a/smart-contracts/test/delegate/DelegateFactory.test.ts b/smart-contracts/test/delegate/DelegateFactory.test.ts new file mode 100644 index 00000000..1020c040 --- /dev/null +++ b/smart-contracts/test/delegate/DelegateFactory.test.ts @@ -0,0 +1,224 @@ +import { + DelegateFactory, + LumerinDiamond, + MorpheusToken, + ProvidersDelegate, + ProvidersDelegate__factory, + UUPSMock, +} from '@ethers-v6'; +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { wei } from '@/scripts/utils/utils'; +import { + deployDelegateFactory, + deployFacetMarketplace, + deployFacetProviderRegistry, + deployLumerinDiamond, + deployMORToken, +} from '@/test/helpers/deployers'; +import { Reverter } from '@/test/helpers/reverter'; + +describe('DelegateFactory', () => { + const reverter = new Reverter(); + + let OWNER: SignerWithAddress; + let KYLE: SignerWithAddress; + let SHEV: SignerWithAddress; + + let diamond: LumerinDiamond; + let delegatorFactory: DelegateFactory; + + let token: MorpheusToken; + + before(async () => { + [OWNER, KYLE, SHEV] = await ethers.getSigners(); + + [diamond, token] = await Promise.all([deployLumerinDiamond(), deployMORToken()]); + await Promise.all([ + deployFacetProviderRegistry(diamond), + deployFacetMarketplace(diamond, token, wei(0.0001), wei(900)), + ]); + + delegatorFactory = await deployDelegateFactory(diamond, 3600); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe('UUPS', () => { + describe('#DelegateFactory_init', () => { + it('should revert if try to call init function twice', async () => { + await expect(delegatorFactory.DelegateFactory_init(OWNER, OWNER, 3600)).to.be.rejectedWith( + 'Initializable: contract is already initialized', + ); + }); + }); + describe('#version', () => { + it('should return correct version', async () => { + expect(await delegatorFactory.version()).to.eq(1); + }); + }); + describe('#upgradeTo', () => { + it('should upgrade to the new version', async () => { + const factory = await ethers.getContractFactory('UUPSMock'); + const newImpl = await factory.deploy(); + + await delegatorFactory.upgradeTo(newImpl); + const newDelegatorFactory = newImpl.attach(delegatorFactory) as UUPSMock; + + expect(await newDelegatorFactory.version()).to.eq(999); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).upgradeTo(KYLE)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + }); + + describe('#setMinDeregistrationTimeout', () => { + it('should set the provider name', async () => { + expect(await delegatorFactory.minDeregistrationTimeout()).eq(3600); + + await delegatorFactory.setMinDeregistrationTimeout(100); + + expect(await delegatorFactory.minDeregistrationTimeout()).eq(100); + }); + it('should throw error when name is zero', async () => { + await expect(delegatorFactory.connect(SHEV).setMinDeregistrationTimeout(100)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#deployProxy', () => { + let providersDelegateFactory: ProvidersDelegate__factory; + + before(async () => { + providersDelegateFactory = await ethers.getContractFactory('ProvidersDelegate'); + }); + + it('should deploy a new proxy', async () => { + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + + const proxy = providersDelegateFactory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegate; + + expect(await proxy.owner()).to.eq(SHEV); + expect(await proxy.fee()).to.eq(wei(0.1, 25)); + expect(await proxy.feeTreasury()).to.eq(KYLE); + expect(await proxy.name()).to.eq('name'); + expect(await proxy.endpoint()).to.eq('endpoint'); + expect(await proxy.deregistrationOpensAt()).to.eq(9998887771); + }); + it('should deploy new proxies', async () => { + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 9998887771); + await delegatorFactory.connect(SHEV).deployProxy(SHEV, wei(0.2, 25), 'name2', 'endpoint2', 9998887772); + await delegatorFactory.connect(KYLE).deployProxy(SHEV, wei(0.3, 25), 'name3', 'endpoint3', 9998887773); + + let proxy = providersDelegateFactory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegate; + expect(await proxy.owner()).to.eq(SHEV); + expect(await proxy.fee()).to.eq(wei(0.2, 25)); + expect(await proxy.feeTreasury()).to.eq(SHEV); + expect(await proxy.name()).to.eq('name2'); + expect(await proxy.endpoint()).to.eq('endpoint2'); + expect(await proxy.deregistrationOpensAt()).to.eq(9998887772); + + proxy = providersDelegateFactory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegate; + expect(await proxy.owner()).to.eq(KYLE); + expect(await proxy.fee()).to.eq(wei(0.3, 25)); + expect(await proxy.feeTreasury()).to.eq(SHEV); + expect(await proxy.name()).to.eq('name3'); + expect(await proxy.endpoint()).to.eq('endpoint3'); + expect(await proxy.deregistrationOpensAt()).to.eq(9998887773); + }); + it('should throw error when fee is invalid', async () => { + await expect( + delegatorFactory.deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 1), + ).to.be.revertedWithCustomError(delegatorFactory, 'InvalidDeregistrationOpenAt'); + }); + }); + + describe('#pause, #unpause', () => { + it('should revert when paused and not after the unpause', async () => { + await delegatorFactory.pause(); + await expect( + delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 9998887771), + ).to.be.rejectedWith('Pausable: paused'); + + await delegatorFactory.unpause(); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name1', 'endpoint1', 9998887771); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).pause()).to.be.revertedWith('Ownable: caller is not the owner'); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).unpause()).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); + + describe('#predictProxyAddress', () => { + it('should predict a proxy address', async () => { + const predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + + const proxyAddress = await delegatorFactory.proxies(SHEV, 0); + + expect(proxyAddress).to.eq(predictedProxyAddress); + }); + it('should predict proxy addresses', async () => { + let predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + expect(await delegatorFactory.proxies(SHEV, 0)).to.eq(predictedProxyAddress); + + predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + expect(await delegatorFactory.proxies(SHEV, 1)).to.eq(predictedProxyAddress); + + predictedProxyAddress = await delegatorFactory.predictProxyAddress(KYLE); + await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + expect(await delegatorFactory.proxies(KYLE, 0)).to.eq(predictedProxyAddress); + + predictedProxyAddress = await delegatorFactory.predictProxyAddress(SHEV); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + expect(await delegatorFactory.proxies(SHEV, 2)).to.eq(predictedProxyAddress); + }); + }); + + describe('#updateImplementation', () => { + it('should update proxies implementation', async () => { + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + await delegatorFactory.connect(SHEV).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + await delegatorFactory.connect(KYLE).deployProxy(KYLE, wei(0.1, 25), 'name', 'endpoint', 9998887771); + + const factory = await ethers.getContractFactory('UUPSMock'); + const newImpl = await factory.deploy(); + + let proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegate; + expect(await proxy.version()).to.eq(1); + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegate; + expect(await proxy.version()).to.eq(1); + proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegate; + expect(await proxy.version()).to.eq(1); + + await delegatorFactory.updateImplementation(newImpl); + + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 0)) as ProvidersDelegate; + expect(await proxy.version()).to.eq(999); + proxy = factory.attach(await delegatorFactory.proxies(SHEV, 1)) as ProvidersDelegate; + expect(await proxy.version()).to.eq(999); + proxy = factory.attach(await delegatorFactory.proxies(KYLE, 0)) as ProvidersDelegate; + expect(await proxy.version()).to.eq(999); + }); + it('should throw error when caller is not an owner', async () => { + await expect(delegatorFactory.connect(KYLE).updateImplementation(KYLE)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); +}); + +// npm run generate-types && npx hardhat test "test/delegate/DelegateFactory.test.ts" +// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/DelegateFactory.test.ts" diff --git a/smart-contracts/test/delegate/ProvidersDelegate.test.ts b/smart-contracts/test/delegate/ProvidersDelegate.test.ts new file mode 100644 index 00000000..bd7fd718 --- /dev/null +++ b/smart-contracts/test/delegate/ProvidersDelegate.test.ts @@ -0,0 +1,554 @@ +import { + LumerinDiamond, + Marketplace, + ModelRegistry, + MorpheusToken, + ProviderRegistry, + ProvidersDelegate, + SessionRouter, +} from '@ethers-v6'; +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { payoutStart } from '../helpers/pool-helper'; + +import { DelegateRegistry } from '@/generated-types/ethers/contracts/mock/delegate-registry/src'; +import { ZERO_ADDR } from '@/scripts/utils/constants'; +import { getHex, wei } from '@/scripts/utils/utils'; +import { + deployDelegateRegistry, + deployFacetDelegation, + deployFacetMarketplace, + deployFacetModelRegistry, + deployFacetProviderRegistry, + deployFacetSessionRouter, + deployLumerinDiamond, + deployMORToken, + deployProvidersDelegate, +} from '@/test/helpers/deployers'; +import { Reverter } from '@/test/helpers/reverter'; +import { setTime } from '@/utils/block-helper'; +import { getProviderApproval, getReceipt } from '@/utils/provider-helper'; +import { DAY, YEAR } from '@/utils/time'; + +describe('ProvidersDelegate', () => { + const reverter = new Reverter(); + + let OWNER: SignerWithAddress; + let DELEGATOR: SignerWithAddress; + let TREASURY: SignerWithAddress; + let KYLE: SignerWithAddress; + let SHEV: SignerWithAddress; + let ALAN: SignerWithAddress; + + let diamond: LumerinDiamond; + let providerRegistry: ProviderRegistry; + let modelRegistry: ModelRegistry; + let providersDelegate: ProvidersDelegate; + let marketplace: Marketplace; + let sessionRouter: SessionRouter; + + let token: MorpheusToken; + let delegateRegistry: DelegateRegistry; + + before(async () => { + [OWNER, DELEGATOR, TREASURY, KYLE, SHEV, ALAN] = await ethers.getSigners(); + + [diamond, token, delegateRegistry] = await Promise.all([ + deployLumerinDiamond(), + deployMORToken(), + deployDelegateRegistry(), + ]); + + [providerRegistry, modelRegistry, sessionRouter, marketplace] = await Promise.all([ + deployFacetProviderRegistry(diamond), + deployFacetModelRegistry(diamond), + deployFacetSessionRouter(diamond, OWNER), + deployFacetMarketplace(diamond, token, wei(0.0001), wei(900)), + deployFacetDelegation(diamond, delegateRegistry), + ]); + + providersDelegate = await deployProvidersDelegate( + diamond, + await TREASURY.getAddress(), + wei(0.2, 25), + 'DLNAME', + 'ENDPOINT', + payoutStart + 3 * DAY, + ); + + await token.transfer(KYLE, wei(1000)); + await token.transfer(SHEV, wei(1000)); + await token.transfer(DELEGATOR, wei(1000)); + await token.transfer(ALAN, wei(1000)); + + await token.connect(OWNER).approve(sessionRouter, wei(1000)); + await token.connect(ALAN).approve(sessionRouter, wei(1000)); + await token.connect(KYLE).approve(providersDelegate, wei(1000)); + await token.connect(SHEV).approve(providersDelegate, wei(1000)); + await token.connect(ALAN).approve(providersDelegate, wei(1000)); + await token.connect(DELEGATOR).approve(modelRegistry, wei(1000)); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe('#providersDelegate_init', () => { + it('should revert if try to call init function twice', async () => { + await expect( + providersDelegate.ProvidersDelegate_init(OWNER, await TREASURY.getAddress(), 1, '', '', 0), + ).to.be.rejectedWith('Initializable: contract is already initialized'); + }); + it('should throw error when fee is invalid', async () => { + await expect( + deployProvidersDelegate(diamond, await TREASURY.getAddress(), wei(1.1, 25), 'DLNAME', 'ENDPOINT', 0n), + ).to.be.revertedWithCustomError(providersDelegate, 'InvalidFee'); + }); + }); + + describe('#setName', () => { + it('should set the provider name', async () => { + await providersDelegate.setName('TEST'); + + expect(await providersDelegate.name()).eq('TEST'); + }); + it('should throw error when name is zero', async () => { + await expect(providersDelegate.setName('')).to.be.revertedWithCustomError(providersDelegate, 'InvalidNameLength'); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegate.connect(KYLE).setName('')).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); + + describe('#setEndpoint', () => { + it('should set the provider endpoint', async () => { + await providersDelegate.setEndpoint('TEST'); + + expect(await providersDelegate.endpoint()).eq('TEST'); + }); + it('should throw error when endpoint is zero', async () => { + await expect(providersDelegate.setEndpoint('')).to.be.revertedWithCustomError( + providersDelegate, + 'InvalidEndpointLength', + ); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegate.connect(KYLE).setEndpoint('')).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#setFeeTreasuryTreasury', () => { + it('should set the provider fee', async () => { + await providersDelegate.setFeeTreasury(KYLE); + + expect(await providersDelegate.feeTreasury()).eq(KYLE); + }); + it('should throw error when fee treasury is invalid', async () => { + await expect(providersDelegate.setFeeTreasury(ZERO_ADDR)).to.be.revertedWithCustomError( + providersDelegate, + 'InvalidFeeTreasuryAddress', + ); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegate.connect(KYLE).setFeeTreasury(KYLE)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#setIsStakeClosed', () => { + it('should set the isStakeClosed flag', async () => { + await providersDelegate.setIsStakeClosed(true); + + expect(await providersDelegate.isStakeClosed()).eq(true); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegate.connect(KYLE).setIsStakeClosed(true)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#stake', () => { + it('should stake tokens, one staker', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + + const staker = await providersDelegate.stakers(KYLE); + expect(staker.staked).to.eq(wei(100)); + expect(staker.pendingRewards).to.eq(wei(0)); + expect(staker.isRestakeDisabled).to.eq(false); + expect(await providersDelegate.totalStaked()).to.eq(wei(100)); + + expect(await token.balanceOf(providersDelegate)).to.eq(wei(0)); + expect(await token.balanceOf(providerRegistry)).to.eq(wei(100)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900)); + }); + it('should stake tokens, two staker', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + + const staker1 = await providersDelegate.stakers(KYLE); + expect(staker1.staked).to.eq(wei(100)); + expect(staker1.pendingRewards).to.eq(wei(0)); + expect(staker1.isRestakeDisabled).to.eq(false); + expect(await providersDelegate.totalStaked()).to.eq(wei(100)); + + await providersDelegate.connect(SHEV).stake(wei(200)); + + const staker2 = await providersDelegate.stakers(SHEV); + expect(staker2.staked).to.eq(wei(200)); + expect(staker2.pendingRewards).to.eq(wei(0)); + expect(staker2.isRestakeDisabled).to.eq(false); + expect(await providersDelegate.totalStaked()).to.eq(wei(300)); + + expect(await token.balanceOf(providersDelegate)).to.eq(wei(0)); + expect(await token.balanceOf(providerRegistry)).to.eq(wei(300)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900)); + expect(await token.balanceOf(SHEV)).to.eq(wei(800)); + }); + it('should throw error when the stake is too low', async () => { + await expect(providersDelegate.connect(KYLE).stake(wei(0))).to.be.revertedWithCustomError( + providersDelegate, + 'InsufficientAmount', + ); + }); + it('should throw error when the stake closed', async () => { + await providersDelegate.setIsStakeClosed(true); + await expect(providersDelegate.connect(KYLE).stake(wei(1))).to.be.revertedWithCustomError( + providersDelegate, + 'StakeClosed', + ); + }); + }); + + describe('#claim', () => { + beforeEach(async () => { + await setTime(5000); + }); + it('should correctly claim, one staker, full claim', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + + await token.transfer(providersDelegate, wei(10)); + + expect(await providersDelegate.getCurrentStakerRewards(KYLE)).to.eq(wei(10)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(908)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(2)); + }); + it('should correctly claim, one staker, partial claim', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + + await token.transfer(providersDelegate, wei(20)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(5)); + expect(await token.balanceOf(KYLE)).to.eq(wei(904)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(1)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(10)); + expect(await token.balanceOf(KYLE)).to.eq(wei(912)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(3)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(5)); + expect(await token.balanceOf(KYLE)).to.eq(wei(916)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); + }); + it('should correctly claim, two stakers, full claim, enter when no rewards distributed', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); + + await token.transfer(providersDelegate, wei(40)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(908)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(2)); + + await providersDelegate.connect(SHEV).claim(SHEV, wei(9999)); + expect(await token.balanceOf(SHEV)).to.eq(wei(724)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(2 + 6)); + }); + it('should correctly claim, two stakers, partial claim, enter when rewards distributed', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + + await token.transfer(providersDelegate, wei(10)); + + await providersDelegate.connect(SHEV).stake(wei(300)); + + await token.transfer(providersDelegate, wei(40)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(916)); // 10 + 25% from 40 + expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); + + await providersDelegate.connect(SHEV).claim(SHEV, wei(20)); + expect(await token.balanceOf(SHEV)).to.eq(wei(716)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4)); + + await token.transfer(providersDelegate, wei(100)); + + await providersDelegate.connect(SHEV).claim(SHEV, wei(20)); + expect(await token.balanceOf(SHEV)).to.eq(wei(732)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(936)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4 + 5)); + + await providersDelegate.connect(SHEV).claim(SHEV, wei(999)); + expect(await token.balanceOf(SHEV)).to.eq(wei(784)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4 + 4 + 4 + 5 + 13)); + }); + it('should throw error when nothing to claim', async () => { + await expect(providersDelegate.connect(KYLE).claim(KYLE, wei(999))).to.be.revertedWithCustomError( + providersDelegate, + 'ClaimAmountIsZero', + ); + }); + }); + + describe('#restake', () => { + beforeEach(async () => { + await setTime(5000); + }); + it('should correctly restake, two stakers, full restake', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); + + await token.transfer(providersDelegate, wei(100)); + + await providersDelegate.connect(OWNER).restake(KYLE, wei(9999)); + expect((await providersDelegate.stakers(KYLE)).staked).to.eq(wei(120)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(5)); + + await token.transfer(providersDelegate, wei(100)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 28.57 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(5 + 28.57 * 0.2), wei(0.01)); + + await providersDelegate.connect(SHEV).claim(SHEV, wei(9999)); + expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 71.42 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(5 + 28.57 * 0.2 + 75 * 0.2 + 71.42 * 0.2), wei(0.01)); + }); + it('should correctly restake, two stakers, partial restake', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); + + await token.transfer(providersDelegate, wei(100)); + + await providersDelegate.connect(OWNER).restake(KYLE, wei(20)); + expect((await providersDelegate.stakers(KYLE)).staked).to.eq(wei(116)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(4)); + + await token.transfer(providersDelegate, wei(100)); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.closeTo(wei(900 + 5 * 0.8 + 27.88 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo(wei(4 + 5 * 0.2 + 27.88 * 0.2), wei(0.01)); + + await providersDelegate.connect(SHEV).claim(SHEV, wei(9999)); + expect(await token.balanceOf(SHEV)).to.closeTo(wei(700 + 75 * 0.8 + 72.11 * 0.8), wei(0.01)); + expect(await token.balanceOf(TREASURY)).to.closeTo( + wei(4 + 5 * 0.2 + 27.88 * 0.2 + 75 * 0.2 + 72.11 * 0.2), + wei(0.01), + ); + }); + it('should correctly restake with zero fee', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + await token.transfer(providersDelegate, wei(10)); + await providersDelegate.connect(OWNER).restake(KYLE, 1); + + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + }); + it('should throw error when restake caller is invalid', async () => { + await expect(providersDelegate.connect(KYLE).restake(SHEV, wei(999))).to.be.revertedWithCustomError( + providersDelegate, + 'RestakeInvalidCaller', + ); + }); + it('should throw error when restake is disabled', async () => { + await providersDelegate.connect(SHEV).setIsRestakeDisabled(true); + await expect(providersDelegate.restake(SHEV, wei(999))).to.be.revertedWithCustomError( + providersDelegate, + 'RestakeDisabled', + ); + }); + }); + + describe('#providerDeregister', () => { + it('should deregister the provider', async () => { + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.providerDeregister([]); + + await providersDelegate.connect(KYLE).claim(KYLE, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(1000)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + }); + it('should throw error when caller is not an owner', async () => { + await expect(providersDelegate.connect(KYLE).providerDeregister([])).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#postModelBid, #deleteModelBids', () => { + const baseModelId = getHex(Buffer.from('1')); + + it('should deregister the model bid and delete it', async () => { + // Register provider + await providersDelegate.connect(SHEV).stake(wei(300)); + + // Register model + await modelRegistry + .connect(DELEGATOR) + .modelRegister(DELEGATOR, baseModelId, getHex(Buffer.from('ipfs://ipfsaddress')), 0, wei(100), 'name', [ + 'tag_1', + ]); + const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); + + // Register bid + const balance = await token.balanceOf(providersDelegate); + await providersDelegate.postModelBid(modelId, wei(0.0001)); + let bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 0); + expect(balance).to.eq(await token.balanceOf(providersDelegate)); + + await providersDelegate.deleteModelBids([bidId]); + + // Register bid again and deregister not from OWNER + await providersDelegate.postModelBid(modelId, wei(0.0001)); + bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 1); + + await setTime(payoutStart + 10 * DAY); + await providersDelegate.connect(ALAN).deleteModelBids([bidId]); + }); + it('should throw error when caller is not an owner for `postModelBid`', async () => { + await expect(providersDelegate.connect(KYLE).postModelBid(baseModelId, wei(0.0001))).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + it('should throw error when caller is not an owner for `deleteModelBids`', async () => { + await expect(providersDelegate.connect(KYLE).deleteModelBids([baseModelId])).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + it('should throw error when try to add bid after the deregistration opened', async () => { + await setTime(payoutStart + 10 * DAY); + + await expect(providersDelegate.postModelBid(baseModelId, wei(0.0001))).to.be.revertedWithCustomError( + providersDelegate, + 'BidCannotBeCreatedDuringThisPeriod', + ); + }); + }); + + describe('#version', () => { + it('should return the correct contract version', async () => { + expect(await providersDelegate.version()).to.eq(1); + }); + }); + + describe('full flow', () => { + const baseModelId = getHex(Buffer.from('1')); + + it('should claim correct reward amount', async () => { + // Register provider + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); + + // Register model + await modelRegistry + .connect(DELEGATOR) + .modelRegister(DELEGATOR, baseModelId, getHex(Buffer.from('ipfs://ipfsaddress')), 0, wei(100), 'name', [ + 'tag_1', + ]); + const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); + + // Register bid + await providersDelegate.postModelBid(modelId, wei(0.0001)); + const bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 0); + + await setTime(payoutStart + 10 * DAY); + const { msg, signature } = await getProviderApproval(OWNER, ALAN, bidId); + await sessionRouter.connect(ALAN).openSession(ALAN, wei(50), false, msg, signature); + const sessionId = await sessionRouter.getSessionId(ALAN, providersDelegate, bidId, 0); + + const sessionTreasuryBalanceBefore = await token.balanceOf(OWNER); + + await setTime(payoutStart + 15 * DAY); + const { msg: receiptMsg } = await getReceipt(OWNER, sessionId, 0, 0); + const { signature: receiptSig } = await getReceipt(OWNER, sessionId, 0, 0); + await sessionRouter.connect(ALAN).closeSession(receiptMsg, receiptSig); + + const sessionTreasuryBalanceAfter = await token.balanceOf(OWNER); + const reward = sessionTreasuryBalanceBefore - sessionTreasuryBalanceAfter; + + await providersDelegate.claim(KYLE, wei(9999)); + await providersDelegate.claim(SHEV, wei(9999)); + expect(await token.balanceOf(KYLE)).to.eq(wei(900) + BigInt(Number(reward.toString()) * 0.25 * 0.8)); + expect(await token.balanceOf(SHEV)).to.eq(wei(700) + BigInt(Number(reward.toString()) * 0.75 * 0.8)); + expect(await token.balanceOf(TREASURY)).to.eq(BigInt(Number(reward.toString()) * 0.2)); + }); + + it('should correctly deregister provider without fees', async () => { + await setTime(payoutStart + 1 * DAY); + + // Register provider + await providersDelegate.connect(KYLE).stake(wei(100)); + await providersDelegate.connect(SHEV).stake(wei(300)); + + // Register model + await modelRegistry + .connect(DELEGATOR) + .modelRegister(DELEGATOR, baseModelId, getHex(Buffer.from('ipfs://ipfsaddress')), 0, wei(100), 'name', [ + 'tag_1', + ]); + const modelId = await modelRegistry.getModelId(DELEGATOR, baseModelId); + + // Register bid + await providersDelegate.postModelBid(modelId, wei(0.0001)); + const bidId = await marketplace.getBidId(await providersDelegate.getAddress(), modelId, 0); + + // Open session + await setTime(payoutStart + 10 * DAY); + const { msg, signature } = await getProviderApproval(OWNER, ALAN, bidId); + await sessionRouter.connect(ALAN).openSession(ALAN, wei(50), false, msg, signature); + const sessionId = await sessionRouter.getSessionId(ALAN, providersDelegate, bidId, 0); + + // Close session + await setTime(payoutStart + 15 * DAY); + const { msg: receiptMsg } = await getReceipt(OWNER, sessionId, 0, 0); + const { signature: receiptSig } = await getReceipt(OWNER, sessionId, 0, 0); + await sessionRouter.connect(ALAN).closeSession(receiptMsg, receiptSig); + + // Add the new Staker + await providersDelegate.connect(ALAN).stake(wei(1000)); + + // Deregister the providers + await providersDelegate.connect(KYLE).providerDeregister([bidId]); + + // Claim rewards + await providersDelegate.claim(KYLE, wei(9999)); + await providersDelegate.claim(SHEV, wei(9999)); + await providersDelegate.claim(ALAN, wei(9999)); + expect(await token.balanceOf(KYLE)).to.closeTo(wei(1000), wei(0.1)); + expect(await token.balanceOf(SHEV)).to.closeTo(wei(1000), wei(0.1)); + expect(await token.balanceOf(ALAN)).to.closeTo(wei(1000), wei(0.2)); + expect(await token.balanceOf(TREASURY)).to.eq(wei(0)); + + // Withdraw all stake after the first limit period + expect((await providerRegistry.getProvider(await providersDelegate.getAddress()))[1]).to.greaterThan(0); + await setTime(payoutStart + 15 * DAY + 1.1 * YEAR); + await providersDelegate.connect(KYLE).stake(wei(1)); + await providersDelegate.connect(KYLE).providerDeregister([]); + expect((await providerRegistry.getProvider(await providersDelegate.getAddress()))[1]).to.eq(0); + }); + }); +}); + +// npm run generate-types && npx hardhat test "test/delegate/ProvidersDelegate.test.ts" +// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/delegate/ProvidersDelegate.test.ts" diff --git a/smart-contracts/test/diamond/facets/SessionRouter.test.ts b/smart-contracts/test/diamond/facets/SessionRouter.test.ts index 8279f3e1..d31f1da4 100644 --- a/smart-contracts/test/diamond/facets/SessionRouter.test.ts +++ b/smart-contracts/test/diamond/facets/SessionRouter.test.ts @@ -238,6 +238,42 @@ describe('SessionRouter', () => { expect(await sessionRouter.getProviderSessions(PROVIDER, 0, 10)).to.deep.eq([[sessionId], 1n]); expect(await sessionRouter.getModelSessions(modelId, 0, 10)).to.deep.eq([[sessionId], 1n]); }); + it('should open a session from the delegatee address with approval from delegatee to delegatee', async () => { + await delegateRegistry + .connect(SECOND) + .delegateContract(OWNER, providerRegistry, await providerRegistry.DELEGATION_RULES_SESSION(), true); + await delegateRegistry + .connect(PROVIDER) + .delegateContract(OWNER, providerRegistry, await providerRegistry.DELEGATION_RULES_PROVIDER(), true); + + await setTime(payoutStart + 10 * DAY); + const { msg, signature } = await getProviderApproval(OWNER, OWNER, bidId); + await sessionRouter.connect(OWNER).openSession(SECOND, wei(50), false, msg, signature); + + const sessionId = await sessionRouter.getSessionId(SECOND, PROVIDER, bidId, 0); + const data = await sessionRouter.getSession(sessionId); + expect(data.user).to.eq(SECOND); + expect(data.bidId).to.eq(bidId); + expect(data.stake).to.eq(wei(50)); + expect(data.closeoutReceipt).to.eq('0x'); + expect(data.closeoutType).to.eq(0); + expect(data.providerWithdrawnAmount).to.eq(0); + expect(data.openedAt).to.eq(payoutStart + 10 * DAY + 1); + expect(data.endsAt).to.greaterThan(data.openedAt); + expect(data.closedAt).to.eq(0); + expect(data.isActive).to.eq(true); + expect(data.isDirectPaymentFromUser).to.eq(false); + + const tokenBalAfter = await token.balanceOf(sessionRouter); + expect(tokenBalAfter - tokenBalBefore).to.eq(wei(50)); + const secondBalAfter = await token.balanceOf(SECOND); + expect(secondBalBefore - secondBalAfter).to.eq(wei(50)); + + expect(await sessionRouter.getIsProviderApprovalUsed(msg)).to.eq(true); + expect(await sessionRouter.getUserSessions(SECOND, 0, 10)).to.deep.eq([[sessionId], 1n]); + expect(await sessionRouter.getProviderSessions(PROVIDER, 0, 10)).to.deep.eq([[sessionId], 1n]); + expect(await sessionRouter.getModelSessions(modelId, 0, 10)).to.deep.eq([[sessionId], 1n]); + }); it('should open two different sessions wit the same input params', async () => { await setTime(payoutStart + 10 * DAY); const { msg: msg1, signature: signature1 } = await getProviderApproval(PROVIDER, SECOND, bidId); @@ -300,25 +336,25 @@ describe('SessionRouter', () => { expect(await sessionRouter.getProviderSessions(PROVIDER, 0, 10)).to.deep.eq([[sessionId], 1n]); expect(await sessionRouter.getModelSessions(modelId, 0, 10)).to.deep.eq([[sessionId], 1n]); }); - it('should throw error when the approval is for an another user', async () => { - const { msg, signature } = await getProviderApproval(PROVIDER, OWNER, bidId); - await expect( - sessionRouter.connect(SECOND).openSession(SECOND, wei(50), false, msg, signature), - ).to.be.revertedWithCustomError(sessionRouter, 'SessionApprovedForAnotherUser'); - }); it('should throw error when the approval is for an another chain', async () => { const { msg, signature } = await getProviderApproval(PROVIDER, SECOND, bidId, 1n); await expect( sessionRouter.connect(SECOND).openSession(SECOND, wei(50), false, msg, signature), - ).to.be.revertedWithCustomError(sessionRouter, 'SesssionApprovedForAnotherChainId'); + ).to.be.revertedWithCustomError(sessionRouter, 'SessionApprovedForAnotherChainId'); }); - it('should throw error when an aprrove expired', async () => { + it('should throw error when an approval expired', async () => { await setTime(payoutStart); const { msg, signature } = await getProviderApproval(PROVIDER, SECOND, bidId); await setTime(payoutStart + 600); await expect( sessionRouter.connect(SECOND).openSession(SECOND, wei(50), false, msg, signature), - ).to.be.revertedWithCustomError(sessionRouter, 'SesssionApproveExpired'); + ).to.be.revertedWithCustomError(sessionRouter, 'SessionApproveExpired'); + }); + it('should throw error when approval is not from provider and without delegation', async () => { + const { msg, signature } = await getProviderApproval(PROVIDER, SECOND, bidId); + await expect( + sessionRouter.connect(OWNER).openSession(PROVIDER, wei(50), false, msg, signature), + ).to.be.revertedWithCustomError(sessionRouter, 'InsufficientRightsForOperation'); }); it('should throw error when the bid is not found', async () => { const { msg, signature } = await getProviderApproval(PROVIDER, SECOND, getHex(Buffer.from('1'))); @@ -672,7 +708,7 @@ describe('SessionRouter', () => { const { msg: receiptMsg, signature: receiptSig } = await getReceipt(PROVIDER, sessionId, 0, 0, 1n); await expect(sessionRouter.connect(SECOND).closeSession(receiptMsg, receiptSig)).to.be.revertedWithCustomError( sessionRouter, - 'SesssionReceiptForAnotherChainId', + 'SessionReceiptForAnotherChainId', ); }); it('should throw error when the provider receipt expired', async () => { @@ -684,7 +720,7 @@ describe('SessionRouter', () => { await setTime(openedAt + 10000); await expect(sessionRouter.connect(SECOND).closeSession(receiptMsg, receiptSig)).to.be.revertedWithCustomError( sessionRouter, - 'SesssionReceiptExpired', + 'SessionReceiptExpired', ); }); }); diff --git a/smart-contracts/test/helpers/deployers/delegate/delegate-factory.ts b/smart-contracts/test/helpers/deployers/delegate/delegate-factory.ts new file mode 100644 index 00000000..a185c522 --- /dev/null +++ b/smart-contracts/test/helpers/deployers/delegate/delegate-factory.ts @@ -0,0 +1,23 @@ +import { ethers } from 'hardhat'; + +import { DelegateFactory, LumerinDiamond } from '@/generated-types/ethers'; + +export const deployDelegateFactory = async ( + diamond: LumerinDiamond, + minDeregistrationTimeout: number, +): Promise => { + const [providersDelegateImplFactory, delegateFactoryImplFactory, proxyFactory] = await Promise.all([ + ethers.getContractFactory('ProvidersDelegate'), + ethers.getContractFactory('DelegateFactory'), + ethers.getContractFactory('ERC1967Proxy'), + ]); + + const delegatorFactoryImpl = await delegateFactoryImplFactory.deploy(); + const proxy = await proxyFactory.deploy(delegatorFactoryImpl, '0x'); + const delegatorFactory = delegatorFactoryImpl.attach(proxy) as DelegateFactory; + + const providersDelegateImpl = await providersDelegateImplFactory.deploy(); + await delegatorFactory.DelegateFactory_init(diamond, providersDelegateImpl, minDeregistrationTimeout); + + return delegatorFactory; +}; diff --git a/smart-contracts/test/helpers/deployers/delegate/index.ts b/smart-contracts/test/helpers/deployers/delegate/index.ts new file mode 100644 index 00000000..bcf0e085 --- /dev/null +++ b/smart-contracts/test/helpers/deployers/delegate/index.ts @@ -0,0 +1,2 @@ +export * from './delegate-factory'; +export * from './providers-delegate'; diff --git a/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts b/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts new file mode 100644 index 00000000..d421fd0a --- /dev/null +++ b/smart-contracts/test/helpers/deployers/delegate/providers-delegate.ts @@ -0,0 +1,26 @@ +import { BigNumberish } from 'ethers'; +import { ethers } from 'hardhat'; + +import { LumerinDiamond, ProvidersDelegate } from '@/generated-types/ethers'; + +export const deployProvidersDelegate = async ( + diamond: LumerinDiamond, + feeTreasury: string, + fee: BigNumberish, + name: string, + endpoint: string, + deregistrationOpensAt_: bigint | number, +): Promise => { + const [implFactory, proxyFactory] = await Promise.all([ + ethers.getContractFactory('ProvidersDelegate'), + ethers.getContractFactory('ERC1967Proxy'), + ]); + + const impl = await implFactory.deploy(); + const proxy = await proxyFactory.deploy(impl, '0x'); + const contract = implFactory.attach(proxy) as ProvidersDelegate; + + await contract.ProvidersDelegate_init(diamond, feeTreasury, fee, name, endpoint, deregistrationOpensAt_); + + return contract; +}; diff --git a/smart-contracts/test/helpers/deployers/index.ts b/smart-contracts/test/helpers/deployers/index.ts index 6d1b8c98..c9eb775c 100644 --- a/smart-contracts/test/helpers/deployers/index.ts +++ b/smart-contracts/test/helpers/deployers/index.ts @@ -1,2 +1,3 @@ +export * from './delegate'; export * from './diamond'; export * from './mock';