diff --git a/README.md b/README.md index feacd424..69feb531 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ This method requests the Bridge contract on RSK a refund for the service. ### **isOperational** - function isOperational(address addr) external view returns (bool) + function isOperational(Flyover.ProviderType providerType, address addr) external view returns (bool) Checks whether a liquidity provider can deliver a pegin service @@ -132,10 +132,6 @@ Checks whether a liquidity provider can deliver a pegin service Whether the liquidity provider is registered and has enough locked collateral -### **isOperationalForPegout** - - function isOperationalForPegout(address addr) external view returns (bool) - Checks whether a liquidity provider can deliver a pegout service #### Parametets diff --git a/contracts/FlyoverDiscovery.sol b/contracts/FlyoverDiscovery.sol new file mode 100644 index 00000000..2820bb08 --- /dev/null +++ b/contracts/FlyoverDiscovery.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +/* solhint-disable comprehensive-interface */ + +import { + AccessControlDefaultAdminRulesUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; +import {ICollateralManagement} from "./interfaces/ICollateralManagement.sol"; +import {IFlyoverDiscovery} from "./interfaces/IFlyoverDiscovery.sol"; +import {Flyover} from "./libraries/Flyover.sol"; + +/// @title FlyoverDiscovery +/// @notice Registry and discovery of Liquidity Providers (LPs) for Flyover +/// @dev Keeps LP metadata and consults `ICollateralManagement` to decide listing and operational status +contract FlyoverDiscovery is + AccessControlDefaultAdminRulesUpgradeable, + IFlyoverDiscovery +{ + + // ------------------------------------------------------------ + // FlyoverDiscovery State Variables + // ------------------------------------------------------------ + + mapping(uint => Flyover.LiquidityProvider) private _liquidityProviders; + ICollateralManagement private _collateralManagement; + uint public lastProviderId; + + // ------------------------------------------------------------ + // FlyoverDiscovery Public Functions and Modifiers + // ------------------------------------------------------------ + + /// @notice Initializes the contract with admin configuration + /// @dev Uses OZ upgradeable admin rules. Must be called only once + /// @param owner The Default Admin and initial owner address + /// @param initialDelay The initial admin delay for `AccessControlDefaultAdminRulesUpgradeable` + /// @param collateralManagement The address of the `ICollateralManagement` contract + function initialize( + address owner, + uint48 initialDelay, + address collateralManagement + ) external initializer { + if (collateralManagement.code.length == 0) revert Flyover.NoContract(collateralManagement); + __AccessControlDefaultAdminRules_init(initialDelay, owner); + _collateralManagement = ICollateralManagement(collateralManagement); + } + + /// @inheritdoc IFlyoverDiscovery + function register( + string calldata name, + string calldata apiBaseUrl, + bool status, + Flyover.ProviderType providerType + ) external payable returns (uint) { + + _validateRegistration(name, apiBaseUrl, providerType, msg.sender, msg.value); + + ++lastProviderId; + _liquidityProviders[lastProviderId] = Flyover.LiquidityProvider({ + id: lastProviderId, + providerAddress: msg.sender, + name: name, + apiBaseUrl: apiBaseUrl, + status: status, + providerType: providerType + }); + emit Register(lastProviderId, msg.sender, msg.value); + _addCollateral(providerType, msg.sender, msg.value); + return (lastProviderId); + } + + /// @inheritdoc IFlyoverDiscovery + function setProviderStatus( + uint providerId, + bool status + ) external { + if (msg.sender != owner() && msg.sender != _liquidityProviders[providerId].providerAddress) { + revert NotAuthorized(msg.sender); + } + _liquidityProviders[providerId].status = status; + emit IFlyoverDiscovery.ProviderStatusSet(providerId, status); + } + + /// @inheritdoc IFlyoverDiscovery + function updateProvider(string calldata name, string calldata apiBaseUrl) external { + if (bytes(name).length < 1 || bytes(apiBaseUrl).length < 1) revert InvalidProviderData(name, apiBaseUrl); + Flyover.LiquidityProvider storage lp; + address providerAddress = msg.sender; + for (uint i = 1; i < lastProviderId + 1; ++i) { + lp = _liquidityProviders[i]; + if (providerAddress == lp.providerAddress) { + lp.name = name; + lp.apiBaseUrl = apiBaseUrl; + emit IFlyoverDiscovery.ProviderUpdate(providerAddress, lp.name, lp.apiBaseUrl); + return; + } + } + revert Flyover.ProviderNotRegistered(providerAddress); + } + + /// @inheritdoc IFlyoverDiscovery + function getProviders() external view returns (Flyover.LiquidityProvider[] memory) { + uint count = 0; + Flyover.LiquidityProvider storage lp; + for (uint i = 1; i < lastProviderId + 1; ++i) { + if (_shouldBeListed(_liquidityProviders[i])) { + ++count; + } + } + Flyover.LiquidityProvider[] memory providersToReturn = new Flyover.LiquidityProvider[](count); + count = 0; + for (uint i = 1; i < lastProviderId + 1; ++i) { + lp = _liquidityProviders[i]; + if (_shouldBeListed(lp)) { + providersToReturn[count] = lp; + ++count; + } + } + return providersToReturn; + } + + /// @inheritdoc IFlyoverDiscovery + function getProvider(address providerAddress) external view returns (Flyover.LiquidityProvider memory) { + return _getProvider(providerAddress); + } + + /// @inheritdoc IFlyoverDiscovery + function isOperational(Flyover.ProviderType providerType, address addr) external view returns (bool) { + return _getProvider(addr).status && + _collateralManagement.isCollateralSufficient(providerType, addr); + } + + // ------------------------------------------------------------ + // Getter Functions + // ------------------------------------------------------------ + + /// @inheritdoc IFlyoverDiscovery + function getProvidersId() external view returns (uint) { + return lastProviderId; + } + + // ------------------------------------------------------------ + // FlyoverDiscovery Private Functions + // ------------------------------------------------------------ + + /// @notice Adds collateral to the collateral management contract based on provider type + /// @dev Distributes collateral between peg-in and peg-out based on provider type + /// @param providerType The type of provider (PegIn, PegOut, or Both) + /// @param providerAddress The address of the provider + /// @param collateralAmount The total amount of collateral to add + function _addCollateral( + Flyover.ProviderType providerType, + address providerAddress, + uint256 collateralAmount + ) private { + if (providerType == Flyover.ProviderType.PegIn) { + _collateralManagement.addPegInCollateralTo{value: collateralAmount}(providerAddress); + } else if (providerType == Flyover.ProviderType.PegOut) { + _collateralManagement.addPegOutCollateralTo{value: collateralAmount}(providerAddress); + } else if (providerType == Flyover.ProviderType.Both) { + uint256 halfAmount = collateralAmount / 2; + uint256 remainder = collateralAmount % 2; + _collateralManagement.addPegInCollateralTo{value: halfAmount + remainder}(providerAddress); + _collateralManagement.addPegOutCollateralTo{value: halfAmount}(providerAddress); + } + } + + /// @notice Checks if a liquidity provider should be listed in the public provider list + /// @dev A provider is listed if it is registered and has status enabled + /// @param lp The liquidity provider storage reference + /// @return True if the provider should be listed, false otherwise + function _shouldBeListed(Flyover.LiquidityProvider storage lp) private view returns(bool){ + return _collateralManagement.isRegistered(lp.providerType, lp.providerAddress) && lp.status; + } + + /// @notice Validates registration parameters and requirements + /// @dev Checks EOA status, data validity, provider type, registration status, and collateral requirements + /// @param name The provider name to validate + /// @param apiBaseUrl The provider API URL to validate + /// @param providerType The provider type to validate + /// @param providerAddress The provider address to validate + /// @param collateralAmount The collateral amount to validate against minimum requirements + function _validateRegistration( + string memory name, + string memory apiBaseUrl, + Flyover.ProviderType providerType, + address providerAddress, + uint256 collateralAmount + ) private view { + if (providerAddress != msg.sender || providerAddress.code.length != 0) revert NotEOA(providerAddress); + + if ( + bytes(name).length < 1 || + bytes(apiBaseUrl).length < 1 + ) { + revert InvalidProviderData(name, apiBaseUrl); + } + + if (providerType > type(Flyover.ProviderType).max) revert InvalidProviderType(providerType); + + if ( + _collateralManagement.getPegInCollateral(providerAddress) > 0 || + _collateralManagement.getPegOutCollateral(providerAddress) > 0 || + _collateralManagement.getResignationBlock(providerAddress) != 0 + ) { + revert AlreadyRegistered(providerAddress); + } + + // Check minimum collateral requirement + uint256 minCollateral = _collateralManagement.getMinCollateral(); + if (providerType == Flyover.ProviderType.Both) { + if (collateralAmount < minCollateral * 2) { + revert InsufficientCollateral(collateralAmount); + } + } else { + if (collateralAmount < minCollateral) { + revert InsufficientCollateral(collateralAmount); + } + } + } + + /// @notice Retrieves a liquidity provider by address + /// @dev Searches through all registered providers to find a match + /// @param providerAddress The address of the provider to find + /// @return The liquidity provider record, reverts if not found + function _getProvider(address providerAddress) private view returns (Flyover.LiquidityProvider memory) { + for (uint i = 1; i < lastProviderId + 1; ++i) { + if (_liquidityProviders[i].providerAddress == providerAddress) { + return _liquidityProviders[i]; + } + } + revert Flyover.ProviderNotRegistered(providerAddress); + } +} diff --git a/contracts/PegOutContract.sol b/contracts/PegOutContract.sol index 62ab925b..dccdd92b 100644 --- a/contracts/PegOutContract.sol +++ b/contracts/PegOutContract.sol @@ -97,7 +97,7 @@ contract PegOutContract is _pegOutQuotes[quoteHash] = quote; _pegOutRegistry[quoteHash].depositTimestamp = block.timestamp; - emit PegOutDeposit(quoteHash, msg.sender, msg.value, block.timestamp); + emit PegOutDeposit(quoteHash, msg.sender, block.timestamp, msg.value); if (dustThreshold > msg.value - requiredAmount) { return; diff --git a/contracts/interfaces/IFlyoverDiscovery.sol b/contracts/interfaces/IFlyoverDiscovery.sol index 95609f3f..b705fda0 100644 --- a/contracts/interfaces/IFlyoverDiscovery.sol +++ b/contracts/interfaces/IFlyoverDiscovery.sol @@ -15,16 +15,50 @@ interface IFlyoverDiscovery { error AlreadyRegistered(address from); error InsufficientCollateral(uint amount); + /// @notice Registers the caller as a Liquidity Provider + /// @dev Reverts if caller is not an EOA, already resigned, provides invalid data, invalid type, or lacks collateral + /// @param name Human-readable LP name + /// @param apiBaseUrl Base URL of the LP public API + /// @param status Initial status flag (enabled/disabled) + /// @param providerType The service type(s) the LP offers + /// @return id The newly assigned LP identifier function register( - string memory name, - string memory apiBaseUrl, + string calldata name, + string calldata apiBaseUrl, bool status, Flyover.ProviderType providerType ) external payable returns (uint); - function updateProvider(string memory name,string memory apiBaseUrl) external; + /// @notice Updates the caller LP metadata + /// @dev Reverts if the caller is not registered or provides invalid fields + /// @param name New LP name + /// @param apiBaseUrl New LP API base URL + function updateProvider(string calldata name, string calldata apiBaseUrl) external; + + /// @notice Updates a provider status flag + /// @dev Callable by the LP itself or the contract owner + /// @param providerId The provider identifier + /// @param status The new status value function setProviderStatus(uint providerId, bool status) external; + + /// @notice Lists LPs that should be visible to users + /// @dev A provider is listed if it has sufficient collateral for at least one side and `status` is true + /// @return providersToReturn Array of LP records to display function getProviders() external view returns (Flyover.LiquidityProvider[] memory); + + /// @notice Returns a single LP by address + /// @param providerAddress The LP address + /// @return provider LP record, reverts if not found function getProvider(address providerAddress) external view returns (Flyover.LiquidityProvider memory); + + /// @notice Checks if an LP can operate for a specific type of operation + /// @dev Ignores the first argument as compatibility stub with legacy signature + /// @param providerType The type of provider + /// @param addr The LP address + /// @return isOp True if registered and peg-in collateral >= min function isOperational(Flyover.ProviderType providerType, address addr) external view returns (bool); + + /// @notice Returns the last assigned provider id + /// @return lastId Last provider id + function getProvidersId() external view returns (uint); } diff --git a/contracts/split/FlyoverDiscovery.sol b/contracts/split/FlyoverDiscovery.sol deleted file mode 100644 index b106a4a3..00000000 --- a/contracts/split/FlyoverDiscovery.sol +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import { - Ownable2StepUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {IFlyoverDiscovery} from "../interfaces/IFlyoverDiscovery.sol"; -import {ICollateralManagement} from "../interfaces/ICollateralManagement.sol"; -import {Flyover} from "../libraries/Flyover.sol"; - -contract FlyoverDiscoveryContract is Ownable2StepUpgradeable, IFlyoverDiscovery { - event CollateralManagementSet(address oldContract, address newContract); - - ICollateralManagement public collateralManagement; - mapping(uint => Flyover.LiquidityProvider) private _liquidityProviders; - uint public lastProviderId; - - function initialize( - address owner, - address collateralManagement_ - ) public initializer { - __Ownable_init(owner); - collateralManagement = ICollateralManagement(collateralManagement_); - } - - function setCollateralManagement(address collateralManagement_) external onlyOwner { - emit CollateralManagementSet(address(collateralManagement), collateralManagement_); - collateralManagement = ICollateralManagement(collateralManagement_); - } - - function register( - string memory name, - string memory apiBaseUrl, - bool status, - Flyover.ProviderType providerType - ) external payable returns (uint) { - _validateRegistration(name, apiBaseUrl, providerType, msg.sender); - - lastProviderId++; - _liquidityProviders[lastProviderId] = Flyover.LiquidityProvider({ - id: lastProviderId, - providerAddress: msg.sender, - name: name, - apiBaseUrl: apiBaseUrl, - status: status, - providerType: providerType - }); - emit IFlyoverDiscovery.Register(lastProviderId, msg.sender, msg.value); - _addCollateral(providerType, msg.sender); - return (lastProviderId); - } - - function getProviders() external view returns (Flyover.LiquidityProvider[] memory) { - Flyover.LiquidityProvider[] memory matchingLps = new Flyover.LiquidityProvider[](lastProviderId); - uint count = 0; - Flyover.LiquidityProvider storage lp; - for (uint i = 1; i <= lastProviderId; i++) { - lp = _liquidityProviders[i]; - if (_shouldBeListed(lp)) { - matchingLps[count] = lp; - count++; - } - } - Flyover.LiquidityProvider[] memory providers = new Flyover.LiquidityProvider[](count); - for (uint i = 0; i < lastProviderId; i++) { - providers[i] = matchingLps[i]; - } - return providers; - } - - function getProvider(address providerAddress) external view returns (Flyover.LiquidityProvider memory) { - return _getProvider(providerAddress); - } - - function _getProvider(address providerAddress) private view returns (Flyover.LiquidityProvider memory) { - for (uint i = 1; i <= lastProviderId; i++) { - if (_liquidityProviders[i].providerAddress == providerAddress) { - return _liquidityProviders[i]; - } - } - revert Flyover.ProviderNotRegistered(providerAddress); - } - - function setProviderStatus( - uint providerId, - bool status - ) external { - if (msg.sender != owner() && msg.sender != _liquidityProviders[providerId].providerAddress) { - revert NotAuthorized(msg.sender); - } - _liquidityProviders[providerId].status = status; - emit IFlyoverDiscovery.ProviderStatusSet(providerId, status); - } - - function updateProvider(string memory name, string memory url) external { - if (bytes(name).length <= 0 || bytes(url).length <= 0) revert InvalidProviderData(name, url); - Flyover.LiquidityProvider storage lp; - address providerAddress = msg.sender; - for (uint i = 1; i <= lastProviderId; i++) { - lp = _liquidityProviders[i]; - if (providerAddress == lp.providerAddress) { - lp.name = name; - lp.apiBaseUrl = url; - emit IFlyoverDiscovery.ProviderUpdate(providerAddress, lp.name, lp.apiBaseUrl); - return; - } - } - revert Flyover.ProviderNotRegistered(providerAddress); - } - - function isOperational(Flyover.ProviderType providerType, address providerAddress) external view returns (bool) { - return collateralManagement.isCollateralSufficient(providerType, providerAddress) && - _getProvider(providerAddress).status; - } - - function _shouldBeListed(Flyover.LiquidityProvider storage lp) private view returns(bool){ - return collateralManagement.isRegistered(lp.providerType, lp.providerAddress) && lp.status; - } - - function _validateRegistration( - string memory name, - string memory apiBaseUrl, - Flyover.ProviderType providerType, - address providerAddress - ) private view { - if (providerAddress != msg.sender || providerAddress.code.length != 0) revert NotEOA(providerAddress); - - if ( - bytes(name).length <= 0 || - bytes(apiBaseUrl).length <= 0 - ) { - revert InvalidProviderData(name, apiBaseUrl); - } - - if (providerType > type(Flyover.ProviderType).max) revert InvalidProviderType(providerType); - - if ( - collateralManagement.getPegInCollateral(providerAddress) > 0 || - collateralManagement.getPegOutCollateral(providerAddress) > 0 || - collateralManagement.getResignationBlock(providerAddress) != 0 - ) { - revert AlreadyRegistered(providerAddress); - } - } - - function _addCollateral( - Flyover.ProviderType providerType, - address providerAddress - ) private { - uint amount = msg.value; - uint minCollateral = collateralManagement.getMinCollateral(); - if (providerType == Flyover.ProviderType.PegIn) { - if (amount < minCollateral) revert InsufficientCollateral(amount); - collateralManagement.addPegInCollateralTo{value: amount}(providerAddress); - } else if (providerType == Flyover.ProviderType.PegOut) { - if (amount < minCollateral) revert InsufficientCollateral(amount); - collateralManagement.addPegOutCollateralTo{value: amount}(providerAddress); - } else { - if (amount < minCollateral * 2) revert InsufficientCollateral(amount); - uint halfMsgValue = amount / 2; - collateralManagement.addPegInCollateralTo{ - value: amount % 2 == 0 ? halfMsgValue : halfMsgValue + 1 - }(providerAddress); - collateralManagement.addPegOutCollateralTo{ - value: halfMsgValue - }(providerAddress); - } - } - -} diff --git a/contracts/split/FlyoverDiscoveryFull.sol b/contracts/split/FlyoverDiscoveryFull.sol deleted file mode 100644 index 011bd7eb..00000000 --- a/contracts/split/FlyoverDiscoveryFull.sol +++ /dev/null @@ -1,396 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {IFlyoverDiscovery} from "../interfaces/IFlyoverDiscovery.sol"; -import {ICollateralManagement} from "../interfaces/ICollateralManagement.sol"; -import {Flyover} from "../libraries/Flyover.sol"; -import {Quotes} from "../libraries/Quotes.sol"; -import { - AccessControlDefaultAdminRulesUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; -import { - ReentrancyGuardUpgradeable -} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; - -contract FlyoverDiscoveryFull is - AccessControlDefaultAdminRulesUpgradeable, - ReentrancyGuardUpgradeable, - IFlyoverDiscovery, - ICollateralManagement -{ - - // ------------------------------------------------------------ - // FlyoverDiscovery State Variables - // ------------------------------------------------------------ - - mapping(uint => Flyover.LiquidityProvider) private _liquidityProviders; - uint public lastProviderId; - - // ------------------------------------------------------------ - // Collateral Management State Variables - // ------------------------------------------------------------ - - bytes32 public constant COLLATERAL_SLASHER = keccak256("COLLATERAL_SLASHER"); - bytes32 public constant COLLATERAL_ADDER = keccak256("COLLATERAL_ADDER"); - - event MinCollateralSet(uint256 oldMinCollateral, uint256 newMinCollateral); - event ResignDelayInBlocksSet(uint oldResignDelayInBlocks, uint newResignDelayInBlocks); - - uint private _minCollateral; - uint private _resignDelayInBlocks; - mapping(address => uint256) private _pegInCollateral; - mapping(address => uint256) private _pegOutCollateral; - mapping(address => uint256) private _resignationBlockNum; - mapping(address => uint256) private _rewards; - uint256 public rewardPercentage; - - // ------------------------------------------------------------ - // FlyoverDiscovery Public Functions and Modifiers - // ------------------------------------------------------------ - - modifier onlyRegisteredForPegIn() { - if (!_isRegistered(Flyover.ProviderType.PegIn, msg.sender)) - revert Flyover.ProviderNotRegistered(msg.sender); - _; - } - - modifier onlyRegisteredForPegOut() { - if (!_isRegistered(Flyover.ProviderType.PegOut, msg.sender)) - revert Flyover.ProviderNotRegistered(msg.sender); - _; - } - - function initialize( - address owner, - uint48 initialDelay, - uint minCollateral, - uint resignDelayInBlocks, - uint rewardPercentage_ - ) public initializer { - __AccessControlDefaultAdminRules_init(initialDelay, owner); - __ReentrancyGuard_init(); - _minCollateral = minCollateral; - _resignDelayInBlocks = resignDelayInBlocks; - rewardPercentage = rewardPercentage_; - } - - function register( - string memory name, - string memory apiBaseUrl, - bool status, - Flyover.ProviderType providerType - ) external payable returns (uint) { - _validateRegistration(name, apiBaseUrl, providerType, msg.sender); - - lastProviderId++; - _liquidityProviders[lastProviderId] = Flyover.LiquidityProvider({ - id: lastProviderId, - providerAddress: msg.sender, - name: name, - apiBaseUrl: apiBaseUrl, - status: status, - providerType: providerType - }); - emit IFlyoverDiscovery.Register(lastProviderId, msg.sender, msg.value); - - _addCollateral(providerType, msg.sender, msg.value); - return (lastProviderId); - } - - function getProviders() external view returns (Flyover.LiquidityProvider[] memory) { - uint count = 0; - Flyover.LiquidityProvider storage lp; - for (uint i = 1; i <= lastProviderId; i++) { - if (_shouldBeListed(_liquidityProviders[i])) { - count++; - } - } - Flyover.LiquidityProvider[] memory providers = new Flyover.LiquidityProvider[](count); - count = 0; - for (uint i = 1; i <= lastProviderId; i++) { - lp = _liquidityProviders[i]; - if (_shouldBeListed(lp)) { - providers[count] = lp; - count++; - } - } - return providers; - } - - function getProvider(address providerAddress) external view returns (Flyover.LiquidityProvider memory) { - return _getProvider(providerAddress); - } - - function setProviderStatus( - uint providerId, - bool status - ) external { - if (msg.sender != owner() && msg.sender != _liquidityProviders[providerId].providerAddress) { - revert NotAuthorized(msg.sender); - } - _liquidityProviders[providerId].status = status; - emit IFlyoverDiscovery.ProviderStatusSet(providerId, status); - } - - function updateProvider(string memory name, string memory url) external { - if (bytes(name).length <= 0 || bytes(url).length <= 0) revert InvalidProviderData(name, url); - Flyover.LiquidityProvider storage lp; - address providerAddress = msg.sender; - for (uint i = 1; i <= lastProviderId; i++) { - lp = _liquidityProviders[i]; - if (providerAddress == lp.providerAddress) { - lp.name = name; - lp.apiBaseUrl = url; - emit IFlyoverDiscovery.ProviderUpdate(providerAddress, lp.name, lp.apiBaseUrl); - return; - } - } - revert Flyover.ProviderNotRegistered(providerAddress); - } - - // ------------------------------------------------------------ - // FlyoverDiscovery Private Functions - // ------------------------------------------------------------ - - function _shouldBeListed(Flyover.LiquidityProvider storage lp) private view returns(bool){ - return _isRegistered(lp.providerType, lp.providerAddress) && lp.status; - } - - function _validateRegistration( - string memory name, - string memory apiBaseUrl, - Flyover.ProviderType providerType, - address providerAddress - ) private view { - if (providerAddress != msg.sender || providerAddress.code.length != 0) revert NotEOA(providerAddress); - - if ( - bytes(name).length <= 0 || - bytes(apiBaseUrl).length <= 0 - ) { - revert InvalidProviderData(name, apiBaseUrl); - } - - if (providerType > type(Flyover.ProviderType).max) revert InvalidProviderType(providerType); - - if ( - _pegInCollateral[providerAddress] > 0 || - _pegOutCollateral[providerAddress] > 0 || - _resignationBlockNum[providerAddress] != 0 - ) { - revert AlreadyRegistered(providerAddress); - } - } - - function _addCollateral( - Flyover.ProviderType providerType, - address providerAddress, - uint amount - ) private { - if (providerType == Flyover.ProviderType.PegIn) { - if (amount < _minCollateral) revert InsufficientCollateral(amount); - _addPegInCollateralTo(providerAddress, amount); - } else if (providerType == Flyover.ProviderType.PegOut) { - if (amount < _minCollateral) revert InsufficientCollateral(amount); - _addPegOutCollateralTo(providerAddress, amount); - } else { - if (amount < _minCollateral * 2) revert InsufficientCollateral(amount); - uint halfMsgValue = amount / 2; - _addPegInCollateralTo(providerAddress, amount % 2 == 0 ? halfMsgValue : halfMsgValue + 1); - _addPegOutCollateralTo(providerAddress, halfMsgValue); - } - } - - function _getProvider(address providerAddress) private view returns (Flyover.LiquidityProvider memory) { - for (uint i = 1; i <= lastProviderId; i++) { - if (_liquidityProviders[i].providerAddress == providerAddress) { - return _liquidityProviders[i]; - } - } - revert Flyover.ProviderNotRegistered(providerAddress); - } - - // ------------------------------------------------------------ - // Collateral Management Public Functions and Modifiers - // ------------------------------------------------------------ - - function setMinCollateral(uint minCollateral) external onlyRole(DEFAULT_ADMIN_ROLE) { - emit MinCollateralSet(_minCollateral, minCollateral); - _minCollateral = minCollateral; - } - - function setResignDelayInBlocks(uint resignDelayInBlocks) external onlyRole(DEFAULT_ADMIN_ROLE) { - emit ResignDelayInBlocksSet(_resignDelayInBlocks, resignDelayInBlocks); - _resignDelayInBlocks = resignDelayInBlocks; - } - - function getPegInCollateral(address addr) external view returns (uint) { - return _pegInCollateral[addr]; - } - - function getPegOutCollateral(address addr) external view returns (uint) { - return _pegOutCollateral[addr]; - } - - function getResignationBlock(address addr) external view returns (uint) { - return _resignationBlockNum[addr]; - } - - function addPegInCollateralTo(address addr) external onlyRole(COLLATERAL_ADDER) payable { - _addPegInCollateralTo(addr, msg.value); - } - - function addPegInCollateral() external onlyRegisteredForPegIn payable { - _addPegInCollateralTo(msg.sender, msg.value); - } - - function addPegOutCollateralTo(address addr) external onlyRole(COLLATERAL_ADDER) payable { - _addPegOutCollateralTo(addr, msg.value); - } - - function addPegOutCollateral() external onlyRegisteredForPegOut payable { - _addPegOutCollateralTo(msg.sender, msg.value); - } - - function getMinCollateral() external view returns (uint) { - return _minCollateral; - } - - function isRegistered(Flyover.ProviderType providerType, address addr) external view returns (bool) { - return _isRegistered(providerType, addr); - } - - function isOperational(Flyover.ProviderType providerType, address addr) external view returns (bool) { - return _isCollateralSufficient(providerType, addr) && _getProvider(addr).status; - } - - function isCollateralSufficient(Flyover.ProviderType providerType, address addr) external view returns (bool) { - return _isCollateralSufficient(providerType, addr); - } - - function withdrawCollateral() external nonReentrant { - address providerAddress = msg.sender; - uint resignationBlock = _resignationBlockNum[providerAddress]; - if (resignationBlock <= 0) revert NotResigned(providerAddress); - if (block.number - resignationBlock < _resignDelayInBlocks) { - revert ResignationDelayNotMet(providerAddress, resignationBlock, _resignDelayInBlocks); - } - - uint amount = _pegOutCollateral[providerAddress] + _pegInCollateral[providerAddress]; - _pegOutCollateral[providerAddress] = 0; - _pegInCollateral[providerAddress] = 0; - _resignationBlockNum[providerAddress] = 0; - - emit WithdrawCollateral(providerAddress, amount); - (bool success,) = providerAddress.call{value: amount}(""); - if (!success) revert WithdrawalFailed(providerAddress, amount); - } - - function resign() external { - address providerAddress = msg.sender; - if (_resignationBlockNum[providerAddress] != 0) revert AlreadyResigned(providerAddress); - if (_pegInCollateral[providerAddress] <= 0 && _pegOutCollateral[providerAddress] <= 0) { - revert Flyover.ProviderNotRegistered(providerAddress); - } - _resignationBlockNum[providerAddress] = block.number; - emit Resigned(providerAddress); - } - - // ------------------------------------------------------------ - // Collateral Management Private Functions - // ------------------------------------------------------------ - - function _addPegInCollateralTo(address addr, uint amount) private { - _pegInCollateral[addr] += amount; - emit ICollateralManagement.PegInCollateralAdded(addr, amount); - } - - function _addPegOutCollateralTo(address addr, uint amount) private { - _pegOutCollateral[addr] += amount; - emit ICollateralManagement.PegOutCollateralAdded(addr, amount); - } - - function _isRegistered(Flyover.ProviderType providerType, address addr) private view returns (bool) { - if (providerType == Flyover.ProviderType.PegIn) { - return _pegInCollateral[addr] > 0 && _resignationBlockNum[addr] == 0; - } else if (providerType == Flyover.ProviderType.PegOut) { - return _pegOutCollateral[addr] > 0 && _resignationBlockNum[addr] == 0; - } else { - return _pegInCollateral[addr] > 0 && _pegOutCollateral[addr] > 0 && _resignationBlockNum[addr] == 0; - } - } - - function _isCollateralSufficient(Flyover.ProviderType providerType, address addr) private view returns (bool) { - if (providerType == Flyover.ProviderType.PegIn) { - return _pegInCollateral[addr] >= _minCollateral && - _resignationBlockNum[addr] == 0; - } else if (providerType == Flyover.ProviderType.PegOut) { - return _pegOutCollateral[addr] >= _minCollateral && - _resignationBlockNum[addr] == 0; - } else { - return _pegInCollateral[addr] >= _minCollateral && - _pegOutCollateral[addr] >= _minCollateral && - _resignationBlockNum[addr] == 0; - } - } - - function slashPegInCollateral( - address punisher, - Quotes.PegInQuote calldata quote, - bytes32 quoteHash - ) external onlyRole(COLLATERAL_SLASHER) { - uint penalty = _min( - quote.penaltyFee, - _pegInCollateral[quote.liquidityProviderRskAddress] - ); - _pegInCollateral[quote.liquidityProviderRskAddress] -= penalty; - uint256 punisherReward = (penalty * rewardPercentage) / 100; - _rewards[punisher] += punisherReward; - emit Penalized(quote.liquidityProviderRskAddress, punisher, quoteHash, Flyover.ProviderType.PegIn, penalty, punisherReward); - } - - function slashPegOutCollateral( - address punisher, - Quotes.PegOutQuote calldata quote, - bytes32 quoteHash - ) external onlyRole(COLLATERAL_SLASHER) { - uint penalty = _min( - quote.penaltyFee, - _pegOutCollateral[quote.lpRskAddress] - ); - _pegOutCollateral[quote.lpRskAddress] -= penalty; - uint256 punisherReward = (penalty * rewardPercentage) / 100; - _rewards[punisher] += punisherReward; - emit Penalized(quote.lpRskAddress, punisher, quoteHash, Flyover.ProviderType.PegOut, penalty, punisherReward); - } - - function getRewards(address addr) external view returns (uint256) { - return _rewards[addr]; - } - - function getPenalties() external pure returns (uint256) { - return 0; - } - - function getRewardPercentage() external pure returns (uint256) { - return 0; - } - - function getResignDelayInBlocks() external pure returns (uint256) { - return 0; - } - - function withdrawRewards() external { - address addr = msg.sender; - uint256 rewards = _rewards[addr]; - if (rewards < 1) revert NothingToWithdraw(addr); - _rewards[addr] = 0; - emit RewardsWithdrawn(addr, rewards); - (bool success,) = addr.call{value: rewards}(""); - if (!success) revert WithdrawalFailed(addr, rewards); - } - - function _min(uint a, uint b) private pure returns (uint) { - return a < b ? a : b; - } -} diff --git a/contracts/test/RegisterCaller.sol b/contracts/test/RegisterCaller.sol new file mode 100644 index 00000000..9ba125f6 --- /dev/null +++ b/contracts/test/RegisterCaller.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +/* solhint-disable comprehensive-interface */ +import {IFlyoverDiscovery} from "../interfaces/IFlyoverDiscovery.sol"; +import {Flyover} from "../libraries/Flyover.sol"; + +contract RegisterCaller { + function callRegister( + address discovery, + string calldata name, + string calldata apiBaseUrl, + bool status, + Flyover.ProviderType providerType + ) external payable { + IFlyoverDiscovery(discovery).register{value: msg.value}( + name, + apiBaseUrl, + status, + providerType + ); + } + + function callRegisterWithTypeUint( + address discovery, + string calldata name, + string calldata apiBaseUrl, + bool status, + uint256 providerTypeRaw + ) external payable { + IFlyoverDiscovery(discovery).register{value: msg.value}( + name, + apiBaseUrl, + status, + Flyover.ProviderType(providerTypeRaw) + ); + } +} diff --git a/test/benchmark.test.ts b/test/benchmark.test.ts index 68432244..4d828788 100644 --- a/test/benchmark.test.ts +++ b/test/benchmark.test.ts @@ -1,57 +1,13 @@ import hre, { upgrades } from "hardhat"; import { ethers } from "hardhat"; -import { deployLbcProxy } from "../scripts/deployment-utils/deploy-proxy"; -import { upgradeLbcProxy } from "../scripts/deployment-utils/upgrade-proxy"; import { CollateralManagementContract, - FlyoverDiscoveryContract, - FlyoverDiscoveryFull, - LiquidityBridgeContractV2, + FlyoverDiscovery, } from "../typechain-types"; import { deploy } from "../scripts/deployment-utils/deploy"; describe("FlyoverDiscovery benchmark", () => { - async function deployLbc() { - const network = hre.network.name; - const deployInfo = await deployLbcProxy(network, { verbose: false }); - await upgradeLbcProxy(network, { verbose: false }); - const lbc: LiquidityBridgeContractV2 = await ethers.getContractAt( - "LiquidityBridgeContractV2", - deployInfo.address - ); - const lbcOwner = await hre.ethers.provider.getSigner(); - - return { lbc, lbcOwner }; - } - - async function deployDiscoveryFull() { - const network = hre.network.name; - const proxyName = "FlyoverDiscoveryFull"; - const owner = await hre.ethers.provider.getSigner(); - const deployed = await deploy(proxyName, network, async () => { - const FlyoverDiscoveryFull = await ethers.getContractFactory(proxyName); - const deployed = await upgrades.deployProxy(FlyoverDiscoveryFull, [ - owner.address, - 5000n, - ethers.parseEther("0.03"), - 60n, - 0n, - ]); - const address = await deployed.getAddress(); - return address; - }); - const discovery: FlyoverDiscoveryFull = await ethers.getContractAt( - proxyName, - deployed.address! - ); - const collateralAdder = await discovery.COLLATERAL_ADDER(); - await discovery - .grantRole(collateralAdder, owner.address) - .then((tx) => tx.wait()); - return { discovery, owner }; - } - - async function deployDiscoverySplit() { + async function deployFlyoverDiscovery() { const network = hre.network.name; const collateralManagementProxy = "CollateralManagementContract"; const owner = await hre.ethers.provider.getSigner(); @@ -76,17 +32,18 @@ describe("FlyoverDiscovery benchmark", () => { collateralManagementDeploy.address! ); - const discoveryProxy = "FlyoverDiscoveryContract"; + const discoveryProxy = "FlyoverDiscovery"; const discoveryDeploy = await deploy(discoveryProxy, network, async () => { const FlyoverDiscovery = await ethers.getContractFactory(discoveryProxy); const deployed = await upgrades.deployProxy(FlyoverDiscovery, [ owner.address, - collateralManagementDeploy.address, + 5000n, + collateralManagementDeploy.address!, ]); const address = await deployed.getAddress(); return address; }); - const discovery: FlyoverDiscoveryContract = await ethers.getContractAt( + const discovery: FlyoverDiscovery = await ethers.getContractAt( discoveryProxy, discoveryDeploy.address! ); @@ -102,47 +59,40 @@ describe("FlyoverDiscovery benchmark", () => { .getSigners() .then((signers) => signers.slice(1)); // 1st is the owner - let { lbc } = await deployLbc(); - let { discovery: discoveryFull } = await deployDiscoveryFull(); - let { discovery } = await deployDiscoverySplit(); + let { discovery } = await deployFlyoverDiscovery(); const providersData = [ { account: accounts[1], - providerType: 2, - oldProviderType: "both", + providerType: 2, // Both providerAddress: accounts[1].address, apiBaseUrl: "https://api.flyover1.com", name: "Flyover1", }, { account: accounts[2], - providerType: 0, - oldProviderType: "pegin", + providerType: 0, // PegIn providerAddress: accounts[2].address, apiBaseUrl: "https://api.flyover2.com", name: "Flyover2", }, { account: accounts[3], - providerType: 1, - oldProviderType: "pegout", + providerType: 1, // PegOut providerAddress: accounts[3].address, apiBaseUrl: "https://api.flyover3.com", name: "Flyover3", }, { account: accounts[4], - providerType: 2, - oldProviderType: "both", + providerType: 2, // Both providerAddress: accounts[4].address, apiBaseUrl: "https://api.flyover4.com", name: "Flyover4", }, { account: accounts[5], - providerType: 2, - oldProviderType: "both", + providerType: 2, // Both providerAddress: accounts[5].address, apiBaseUrl: "https://api.flyover5.com", name: "Flyover5", @@ -150,8 +100,7 @@ describe("FlyoverDiscovery benchmark", () => { ]; for (const providerData of providersData) { - const { providerType, oldProviderType, apiBaseUrl, account, name } = - providerData; + const { providerType, apiBaseUrl, account, name } = providerData; discovery = discovery.connect(account); await discovery @@ -159,105 +108,30 @@ describe("FlyoverDiscovery benchmark", () => { value: ethers.parseEther("0.06"), }) .then((tx) => tx.wait()); - discoveryFull = discoveryFull.connect(account); - await discoveryFull - .register(name, apiBaseUrl, true, providerType, { - value: ethers.parseEther("0.06"), - }) - .then((tx) => tx.wait()); - lbc = lbc.connect(account); - await lbc - .register(name, apiBaseUrl, true, oldProviderType, { - value: ethers.parseEther("0.06"), - }) - .then((tx) => tx.wait()); } console.log( "-------------------------------- GET PROVIDERS --------------------------------" ); - console.log( - "-------------------------------- DISCOVERY --------------------------------" - ); const discoveryProviders = await discovery.getProviders(); console.log(discoveryProviders); - console.log( - "-------------------------------- DISCOVERY FULL --------------------------------" - ); - const discoveryFullProviders = await discoveryFull.getProviders(); - console.log(discoveryFullProviders); - console.log( - "-------------------------------- LBC --------------------------------" - ); - const lbcProviders = await lbc.getProviders(); - console.log(lbcProviders); console.log( "-------------------------------- GET PROVIDER --------------------------------" ); - console.log( - "-------------------------------- DISCOVERY --------------------------------" - ); for (const account of providersData) { const result = await discovery.getProvider(account.providerAddress); console.log(result); } - console.log( - "-------------------------------- DISCOVERY FULL --------------------------------" - ); - for (const account of providersData) { - const result = await discoveryFull.getProvider(account.providerAddress); - console.log(result); - } - console.log( - "-------------------------------- LBC --------------------------------" - ); - for (const account of providersData) { - const result = await lbc.getProvider(account.providerAddress); - console.log(result); - } console.log( "-------------------------------- IS OPERATIONAL --------------------------------" ); - const types = [ - { new: 0, old: "pegin" }, - { new: 1, old: "pegout" }, - { new: 2, old: "both" }, - ]; - console.log( - "-------------------------------- DISCOVERY --------------------------------" - ); - for (const account of providersData) { - for (const type of types) { - const result = await discovery.isOperational( - type.new, - account.providerAddress - ); - console.log(account.name, type.old, result); - } - } - console.log( - "-------------------------------- DISCOVERY FULL --------------------------------" - ); - for (const account of providersData) { - for (const type of types) { - const result = await discoveryFull.isOperational( - type.new, - account.providerAddress - ); - console.log(account.name, type.old, result); - } - } - console.log( - "-------------------------------- LBC --------------------------------" - ); for (const account of providersData) { - const peginResult = await lbc.isOperational(account.providerAddress); - const pegoutResult = await lbc.isOperationalForPegout( + const result = await discovery.isOperational( + account.providerType, account.providerAddress ); - console.log(account.name, "pegin", peginResult); - console.log(account.name, "pegout", pegoutResult); + console.log(account.name, "operational:", result); } }); }); diff --git a/test/discovery/collateral-allocation.test.ts b/test/discovery/collateral-allocation.test.ts new file mode 100644 index 00000000..bf3da316 --- /dev/null +++ b/test/discovery/collateral-allocation.test.ts @@ -0,0 +1,278 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { deployDiscoveryFixture } from "./fixtures"; +import { ProviderType } from "../utils/constants"; + +describe("FlyoverDiscovery collateral allocation", () => { + it("correctly allocates collateral for ProviderType.PegIn", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const lp = signers.at(-1)!; + + const collateralAmount = MIN_COLLATERAL; + await discovery + .connect(lp) + .register("PegIn LP", "http://localhost/api", true, ProviderType.PegIn, { + value: collateralAmount, + }); + + // Verify collateral allocation in CollateralManagement contract + expect(await collateralManagement.getPegInCollateral(lp.address)).to.equal( + collateralAmount + ); + expect(await collateralManagement.getPegOutCollateral(lp.address)).to.equal( + 0n + ); + }); + + it("correctly allocates collateral for ProviderType.PegOut", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const lp = signers.at(-2)!; + + const collateralAmount = MIN_COLLATERAL; + await discovery + .connect(lp) + .register( + "PegOut LP", + "http://localhost/api", + true, + ProviderType.PegOut, + { + value: collateralAmount, + } + ); + + // Verify collateral allocation in CollateralManagement contract + expect(await collateralManagement.getPegInCollateral(lp.address)).to.equal( + 0n + ); + expect(await collateralManagement.getPegOutCollateral(lp.address)).to.equal( + collateralAmount + ); + }); + + it("correctly allocates collateral for ProviderType.Both with even amount", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const lp = signers.at(-3)!; + + const collateralAmount = MIN_COLLATERAL * 2n; // Even amount + await discovery + .connect(lp) + .register("Both LP", "http://localhost/api", true, ProviderType.Both, { + value: collateralAmount, + }); + + // Verify collateral allocation in CollateralManagement contract + const expectedHalf = collateralAmount / 2n; + expect(await collateralManagement.getPegInCollateral(lp.address)).to.equal( + expectedHalf + ); + expect(await collateralManagement.getPegOutCollateral(lp.address)).to.equal( + expectedHalf + ); + }); + + it("correctly allocates collateral for ProviderType.Both with odd amount", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const lp = signers.at(-4)!; + + const collateralAmount = MIN_COLLATERAL * 2n + 1n; // Odd amount + await discovery + .connect(lp) + .register( + "Both LP Odd", + "http://localhost/api", + true, + ProviderType.Both, + { + value: collateralAmount, + } + ); + + // Verify collateral allocation in CollateralManagement contract + const halfAmount = collateralAmount / 2n; + const remainder = collateralAmount % 2n; + const expectedPegIn = halfAmount + remainder; // Should get the extra 1 + const expectedPegOut = halfAmount; + + expect(await collateralManagement.getPegInCollateral(lp.address)).to.equal( + expectedPegIn + ); + expect(await collateralManagement.getPegOutCollateral(lp.address)).to.equal( + expectedPegOut + ); + + // Verify total allocation equals the original amount + const totalAllocated = + (await collateralManagement.getPegInCollateral(lp.address)) + + (await collateralManagement.getPegOutCollateral(lp.address)); + expect(totalAllocated).to.equal(collateralAmount); + }); + + it("handles edge case with minimum odd amount for ProviderType.Both", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const lp = signers.at(-5)!; + + // Test with minimum required amount + 1 (odd) + const collateralAmount = MIN_COLLATERAL * 2n + 1n; + await discovery + .connect(lp) + .register( + "Both LP Min Odd", + "http://localhost/api", + true, + ProviderType.Both, + { + value: collateralAmount, + } + ); + + // Verify the allocation + const halfAmount = collateralAmount / 2n; + expect(await collateralManagement.getPegInCollateral(lp.address)).to.equal( + halfAmount + 1n + ); + expect(await collateralManagement.getPegOutCollateral(lp.address)).to.equal( + halfAmount + ); + }); + + it("verifies collateral is actually transferred to CollateralManagement contract", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const lp = signers.at(-6)!; + + // Get initial balance of CollateralManagement contract + const initialBalance = await ethers.provider.getBalance( + await collateralManagement.getAddress() + ); + + const collateralAmount = MIN_COLLATERAL; + await discovery + .connect(lp) + .register("Test LP", "http://localhost/api", true, ProviderType.PegIn, { + value: collateralAmount, + }); + + // Verify the CollateralManagement contract received the funds + const finalBalance = await ethers.provider.getBalance( + await collateralManagement.getAddress() + ); + expect(finalBalance - initialBalance).to.equal(collateralAmount); + }); + + it("verifies total collateral allocation matches sent amount for all provider types", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const [lp1, lp2, lp3] = signers.slice(-3); + + // Test PegIn + const pegInAmount = MIN_COLLATERAL; + await discovery + .connect(lp1) + .register("PegIn LP", "http://localhost/api", true, ProviderType.PegIn, { + value: pegInAmount, + }); + + let totalAllocated = + (await collateralManagement.getPegInCollateral(lp1.address)) + + (await collateralManagement.getPegOutCollateral(lp1.address)); + expect(totalAllocated).to.equal(pegInAmount); + + // Test PegOut + const pegOutAmount = MIN_COLLATERAL; + await discovery + .connect(lp2) + .register( + "PegOut LP", + "http://localhost/api", + true, + ProviderType.PegOut, + { + value: pegOutAmount, + } + ); + + totalAllocated = + (await collateralManagement.getPegInCollateral(lp2.address)) + + (await collateralManagement.getPegOutCollateral(lp2.address)); + expect(totalAllocated).to.equal(pegOutAmount); + + // Test Both with odd amount + const bothAmount = MIN_COLLATERAL * 2n + 3n; // Odd amount + await discovery + .connect(lp3) + .register("Both LP", "http://localhost/api", true, ProviderType.Both, { + value: bothAmount, + }); + + totalAllocated = + (await collateralManagement.getPegInCollateral(lp3.address)) + + (await collateralManagement.getPegOutCollateral(lp3.address)); + expect(totalAllocated).to.equal(bothAmount); + }); + + it("emits correct events for collateral allocation", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const lp = signers.at(-7)!; + + const collateralAmount = MIN_COLLATERAL; + const tx = await discovery + .connect(lp) + .register("Event LP", "http://localhost/api", true, ProviderType.PegIn, { + value: collateralAmount, + }); + + // Verify the Register event + await expect(tx) + .to.emit(discovery, "Register") + .withArgs(1n, lp.address, collateralAmount); + + // Verify the PegInCollateralAdded event + await expect(tx) + .to.emit(collateralManagement, "PegInCollateralAdded") + .withArgs(lp.address, collateralAmount); + }); + + it("emits correct events for ProviderType.Both allocation", async () => { + const { discovery, collateralManagement, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const lp = signers.at(-8)!; + + const collateralAmount = MIN_COLLATERAL * 2n + 1n; // Odd amount + const tx = await discovery + .connect(lp) + .register( + "Both Event LP", + "http://localhost/api", + true, + ProviderType.Both, + { + value: collateralAmount, + } + ); + + // Verify the Register event + await expect(tx) + .to.emit(discovery, "Register") + .withArgs(1n, lp.address, collateralAmount); + + // Verify both collateral events + const halfAmount = collateralAmount / 2n; + const remainder = collateralAmount % 2n; + + await expect(tx) + .to.emit(collateralManagement, "PegInCollateralAdded") + .withArgs(lp.address, halfAmount + remainder); + + await expect(tx) + .to.emit(collateralManagement, "PegOutCollateralAdded") + .withArgs(lp.address, halfAmount); + }); +}); diff --git a/test/discovery/events.test.ts b/test/discovery/events.test.ts new file mode 100644 index 00000000..9bb215b2 --- /dev/null +++ b/test/discovery/events.test.ts @@ -0,0 +1,32 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { + deployDiscoveryFixture, + deployDiscoveryWithProvidersFixture, +} from "./fixtures"; +import { ProviderType } from "../utils/constants"; + +describe("FlyoverDiscovery events", () => { + it("emits Register with id, sender, and amount", async () => { + const { discovery, signers, MIN_COLLATERAL } = await loadFixture( + deployDiscoveryFixture + ); + const lp = signers.at(-1)!; + await expect( + discovery + .connect(lp) + .register("N", "U", true, ProviderType.PegIn, { value: MIN_COLLATERAL }) + ) + .to.emit(discovery, "Register") + .withArgs(1n, lp.address, MIN_COLLATERAL); + }); + + it("emits ProviderStatusSet when toggling status", async () => { + const { discovery, pegOutLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + await expect(discovery.connect(pegOutLp).setProviderStatus(2, false)) + .to.emit(discovery, "ProviderStatusSet") + .withArgs(2n, false); + }); +}); diff --git a/test/discovery/fixtures.ts b/test/discovery/fixtures.ts new file mode 100644 index 00000000..d14f3729 --- /dev/null +++ b/test/discovery/fixtures.ts @@ -0,0 +1,80 @@ +import { upgrades, ethers } from "hardhat"; +import { ProviderType } from "../utils/constants"; +import { deployCollateralManagement } from "../collateral/fixtures"; + +export async function deployDiscoveryFixture() { + const FlyoverDiscovery = await ethers.getContractFactory("FlyoverDiscovery"); + + // Use the existing CollateralManagement fixture instead of manual deployment + const { collateralManagement, signers, owner } = + await deployCollateralManagement(); + + const MIN_COLLATERAL = ethers.parseEther("0.6"); + const INITIAL_DELAY = 500n; + + const discovery = await upgrades.deployProxy(FlyoverDiscovery, [ + owner.address, + INITIAL_DELAY, + await collateralManagement.getAddress(), + ]); + + // Allow owner to add collateral directly for test setup + await collateralManagement + .connect(owner) + .grantRole(await collateralManagement.COLLATERAL_ADDER(), owner.address); + + // Grant COLLATERAL_ADDER role to FlyoverDiscovery contract + await collateralManagement + .connect(owner) + .grantRole( + await collateralManagement.COLLATERAL_ADDER(), + await discovery.getAddress() + ); + + return { + discovery, + collateralManagement, + owner, + signers, + MIN_COLLATERAL, + }; +} + +export async function deployDiscoveryWithProvidersFixture() { + const { discovery, collateralManagement, owner, signers, MIN_COLLATERAL } = + await deployDiscoveryFixture(); + + const pegInLp = signers.pop(); + const pegOutLp = signers.pop(); + const fullLp = signers.pop(); + if (!pegInLp || !pegOutLp || !fullLp) + throw new Error("LP can't be undefined"); + + // Register providers (Discovery now handles collateral addition automatically) + await discovery + .connect(pegInLp) + .register("Pegin Provider", "lp1.com", true, ProviderType.PegIn, { + value: MIN_COLLATERAL, + }); + await discovery + .connect(pegOutLp) + .register("PegOut Provider", "lp2.com", true, ProviderType.PegOut, { + value: MIN_COLLATERAL, + }); + await discovery + .connect(fullLp) + .register("Full Provider", "lp3.com", true, ProviderType.Both, { + value: MIN_COLLATERAL * 2n, + }); + + return { + discovery, + collateralManagement, + owner, + pegInLp, + pegOutLp, + fullLp, + signers, + MIN_COLLATERAL, + }; +} diff --git a/test/discovery/getters.test.ts b/test/discovery/getters.test.ts new file mode 100644 index 00000000..9193464e --- /dev/null +++ b/test/discovery/getters.test.ts @@ -0,0 +1,63 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { deployDiscoveryWithProvidersFixture } from "./fixtures"; +import { ProviderType } from "../utils/constants"; + +describe("FlyoverDiscovery getters", () => { + it("lists registered providers with correct fields", async () => { + const { discovery, pegInLp, pegOutLp, fullLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + + const providers = await discovery.getProviders(); + expect(providers.length).to.equal(3); + + const [p1, p2, p3] = providers; + + expect(p1.id).to.equal(1n); + expect(p1.providerAddress).to.equal(pegInLp.address); + expect(p1.name).to.equal("Pegin Provider"); + expect(p1.apiBaseUrl).to.equal("lp1.com"); + expect(p1.status).to.equal(true); + expect(p1.providerType).to.equal(ProviderType.PegIn); + + expect(p2.id).to.equal(2n); + expect(p2.providerAddress).to.equal(pegOutLp.address); + expect(p2.name).to.equal("PegOut Provider"); + expect(p2.apiBaseUrl).to.equal("lp2.com"); + expect(p2.status).to.equal(true); + expect(p2.providerType).to.equal(ProviderType.PegOut); + + expect(p3.id).to.equal(3n); + expect(p3.providerAddress).to.equal(fullLp.address); + expect(p3.name).to.equal("Full Provider"); + expect(p3.apiBaseUrl).to.equal("lp3.com"); + expect(p3.status).to.equal(true); + expect(p3.providerType).to.equal(ProviderType.Both); + }); + + it("gets a provider by address", async () => { + const { discovery, pegOutLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const provider = await discovery.getProvider(pegOutLp.address); + expect(provider.id).to.equal(2n); + expect(provider.providerAddress).to.equal(pegOutLp.address); + expect(provider.name).to.equal("PegOut Provider"); + expect(provider.apiBaseUrl).to.equal("lp2.com"); + expect(provider.status).to.equal(true); + expect(provider.providerType).to.equal(ProviderType.PegOut); + }); + + it("reverts when getting a non-existing provider", async () => { + const { discovery } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const nonLp = ethers.Wallet.createRandom().address; + await expect(discovery.getProvider(nonLp)).to.be.revertedWithCustomError( + discovery, + "ProviderNotRegistered" + ); + }); +}); diff --git a/test/discovery/listing-filter.test.ts b/test/discovery/listing-filter.test.ts new file mode 100644 index 00000000..11b0131a --- /dev/null +++ b/test/discovery/listing-filter.test.ts @@ -0,0 +1,68 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { + deployDiscoveryWithProvidersFixture, + deployDiscoveryFixture, +} from "./fixtures"; +import { ProviderType } from "../utils/constants"; + +describe("FlyoverDiscovery listing filters", () => { + it("lists only enabled providers", async () => { + const { discovery, pegOutLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + + let providers = await discovery.getProviders(); + expect(providers.map((p) => p.id)).to.deep.equal([1n, 2n, 3n]); + + // Disable provider with id 2 + await discovery.connect(pegOutLp).setProviderStatus(2, false); + + providers = await discovery.getProviders(); + expect(providers.map((p) => p.id)).to.deep.equal([1n, 3n]); + }); +}); + +describe("FlyoverDiscovery listing edge cases", () => { + it("lists providers immediately after registration since collateral is added automatically", async () => { + const { discovery, signers, MIN_COLLATERAL } = await loadFixture( + deployDiscoveryFixture + ); + const lp = signers.at(-1)!; + + await discovery + .connect(lp) + .register("N", "U", true, ProviderType.PegIn, { value: MIN_COLLATERAL }); + + const providers = await discovery.getProviders(); + // Provider is immediately listed because collateral is added automatically during registration + expect(providers.length).to.equal(1); + expect(providers[0].providerAddress).to.equal(lp.address); + }); + + it("returns providers ordered by id", async () => { + const { discovery, collateralManagement, owner, signers, MIN_COLLATERAL } = + await loadFixture(deployDiscoveryFixture); + const [a, b, c] = signers.slice(-3); + + await discovery + .connect(a) + .register("A", "U1", true, ProviderType.PegIn, { value: MIN_COLLATERAL }); + await discovery + .connect(b) + .register("B", "U2", true, ProviderType.PegIn, { value: MIN_COLLATERAL }); + await discovery + .connect(c) + .register("C", "U3", true, ProviderType.PegIn, { value: MIN_COLLATERAL }); + + // fund collateral to list them + for (const lp of [a, b, c]) { + await collateralManagement + .connect(owner) + .addPegInCollateralTo(lp.address, { value: MIN_COLLATERAL }); + } + + const providers = await discovery.getProviders(); + expect(providers.map((p) => p.id)).to.deep.equal([1n, 2n, 3n]); + }); +}); diff --git a/test/discovery/not-eoa.test.ts b/test/discovery/not-eoa.test.ts new file mode 100644 index 00000000..e7583438 --- /dev/null +++ b/test/discovery/not-eoa.test.ts @@ -0,0 +1,26 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { deployDiscoveryFixture } from "./fixtures"; +import { ProviderType } from "../utils/constants"; + +describe("FlyoverDiscovery NotEOA checks", () => { + it("reverts when a contract calls register (NotEOA)", async () => { + const { discovery, MIN_COLLATERAL } = await loadFixture( + deployDiscoveryFixture + ); + const RegisterCaller = await ethers.getContractFactory("RegisterCaller"); + const caller = await RegisterCaller.deploy(); + await caller.waitForDeployment(); + await expect( + caller.callRegister( + await discovery.getAddress(), + "N", + "U", + true, + ProviderType.PegIn, + { value: MIN_COLLATERAL } + ) + ).to.be.revertedWithCustomError(discovery, "NotEOA"); + }); +}); diff --git a/test/discovery/operational.test.ts b/test/discovery/operational.test.ts new file mode 100644 index 00000000..19a57e5d --- /dev/null +++ b/test/discovery/operational.test.ts @@ -0,0 +1,45 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { deployDiscoveryWithProvidersFixture } from "./fixtures"; +import { ProviderType } from "../utils/constants"; + +describe("FlyoverDiscovery operational checks", () => { + it("isOperational returns true only for providers with sufficient collateral for their type", async () => { + const { discovery, pegInLp, fullLp, pegOutLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + + // Test PegIn operations + expect( + await discovery.isOperational(ProviderType.PegIn, pegInLp.address) + ).to.equal(true); + expect( + await discovery.isOperational(ProviderType.PegIn, fullLp.address) + ).to.equal(true); + expect( + await discovery.isOperational(ProviderType.PegIn, pegOutLp.address) + ).to.equal(false); + + // Test PegOut operations + expect( + await discovery.isOperational(ProviderType.PegOut, pegInLp.address) + ).to.equal(false); + expect( + await discovery.isOperational(ProviderType.PegOut, fullLp.address) + ).to.equal(true); + expect( + await discovery.isOperational(ProviderType.PegOut, pegOutLp.address) + ).to.equal(true); + + // Test Both operations (requires sufficient collateral for both PegIn AND PegOut) + expect( + await discovery.isOperational(ProviderType.Both, pegInLp.address) + ).to.equal(false); // Only has PegIn collateral + expect( + await discovery.isOperational(ProviderType.Both, pegOutLp.address) + ).to.equal(false); // Only has PegOut collateral + expect( + await discovery.isOperational(ProviderType.Both, fullLp.address) + ).to.equal(true); // Has both PegIn and PegOut collateral + }); +}); diff --git a/test/discovery/registration.test.ts b/test/discovery/registration.test.ts new file mode 100644 index 00000000..315fa0dd --- /dev/null +++ b/test/discovery/registration.test.ts @@ -0,0 +1,147 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { + deployDiscoveryFixture, + deployDiscoveryWithProvidersFixture, +} from "./fixtures"; +import { ProviderType } from "../utils/constants"; + +describe("FlyoverDiscovery registration", () => { + it("registers providers and increments lastProviderId", async () => { + const { discovery, signers, MIN_COLLATERAL } = await loadFixture( + deployDiscoveryFixture + ); + const [lp1, lp2, lp3] = signers.slice(-3); + + const tx1 = await discovery + .connect(lp1) + .register("LP1", "http://localhost/api1", true, ProviderType.Both, { + value: MIN_COLLATERAL * 2n, + }); + await expect(tx1).to.emit(discovery, "Register"); + + const tx2 = await discovery + .connect(lp2) + .register("LP2", "http://localhost/api2", true, ProviderType.PegIn, { + value: MIN_COLLATERAL, + }); + await expect(tx2).to.emit(discovery, "Register"); + + const tx3 = await discovery + .connect(lp3) + .register("LP3", "http://localhost/api3", true, ProviderType.PegOut, { + value: MIN_COLLATERAL, + }); + await expect(tx3).to.emit(discovery, "Register"); + + const lastId = await discovery.getProvidersId(); + expect(lastId).to.equal(3n); + }); + + it("reverts on invalid registration data (empty name/url)", async () => { + const { discovery, signers, MIN_COLLATERAL } = await loadFixture( + deployDiscoveryFixture + ); + const lp = signers.at(-1)!; + + await expect( + discovery + .connect(lp) + .register("", "http://localhost/api", true, ProviderType.PegIn, { + value: MIN_COLLATERAL, + }) + ).to.be.revertedWithCustomError(discovery, "InvalidProviderData"); + + await expect( + discovery.connect(lp).register("LP", "", true, ProviderType.PegIn, { + value: MIN_COLLATERAL, + }) + ).to.be.revertedWithCustomError(discovery, "InvalidProviderData"); + }); + + it("reverts on insufficient collateral depending on provider type", async () => { + const { discovery, signers, MIN_COLLATERAL } = await loadFixture( + deployDiscoveryFixture + ); + const [lpBoth, lpIn, lpOut] = signers.slice(-3); + + await expect( + discovery + .connect(lpBoth) + .register("LPB", "url", true, ProviderType.Both, { + value: MIN_COLLATERAL, // needs 2x + }) + ).to.be.revertedWithCustomError(discovery, "InsufficientCollateral"); + + await expect( + discovery.connect(lpIn).register("LPI", "url", true, ProviderType.PegIn, { + value: MIN_COLLATERAL - 1n, + }) + ).to.be.revertedWithCustomError(discovery, "InsufficientCollateral"); + + await expect( + discovery + .connect(lpOut) + .register("LPO", "url", true, ProviderType.PegOut, { + value: MIN_COLLATERAL - 1n, + }) + ).to.be.revertedWithCustomError(discovery, "InsufficientCollateral"); + }); + + it("returns the last provider id after pre-registered providers", async () => { + const { discovery } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const lastId = await discovery.getProvidersId(); + expect(lastId).to.equal(3n); + }); +}); + +describe("FlyoverDiscovery registration edge cases", () => { + it("reverts when providerType is invalid", async () => { + const { discovery, MIN_COLLATERAL } = await loadFixture( + deployDiscoveryFixture + ); + const RegisterCaller = await ( + await import("hardhat") + ).ethers.getContractFactory("RegisterCaller"); + const caller = await RegisterCaller.deploy(); + await caller.waitForDeployment(); + // Note: With the current function signature (enum parameter), the ABI decoder + // reverts with panic 0x21 for values outside the enum before the function body + // executes, so the contract's InvalidProviderType custom error cannot be reached. + // To assert the custom error instead, the contract would need to accept a raw + // uint8 and validate inside before casting to the enum. + await expect( + caller.callRegisterWithTypeUint( + await discovery.getAddress(), + "N", + "U", + true, + 999, + { value: MIN_COLLATERAL } + ) + ).to.be.revertedWithPanic(0x21); + }); + + it("prevents multiple registrations by the same EOA", async () => { + const { discovery, signers, MIN_COLLATERAL } = await loadFixture( + deployDiscoveryFixture + ); + const lp = signers.at(-1)!; + await discovery.connect(lp).register("N1", "U1", true, ProviderType.PegIn, { + value: MIN_COLLATERAL, + }); + + // Second registration by the same EOA should fail + await expect( + discovery.connect(lp).register("N2", "U2", true, ProviderType.PegOut, { + value: MIN_COLLATERAL, + }) + ).to.be.revertedWithCustomError(discovery, "AlreadyRegistered"); + + const providers = await discovery.getProviders(); + expect(providers.length).to.equal(1); + expect(providers[0].providerAddress).to.equal(lp.address); + }); +}); diff --git a/test/discovery/resign.test.ts b/test/discovery/resign.test.ts new file mode 100644 index 00000000..fd01516a --- /dev/null +++ b/test/discovery/resign.test.ts @@ -0,0 +1,264 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import * as hardhatHelpers from "@nomicfoundation/hardhat-network-helpers"; +import { ethers } from "hardhat"; +import { deployDiscoveryWithProvidersFixture } from "./fixtures"; +import { + createBalanceDifferenceAssertion, + createBalanceUpdateAssertion, +} from "../utils/asserts"; + +describe("Discovery resign flow should", () => { + it("emit Resigned and hide provider from listing", async () => { + const fixtureResult = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const { discovery, collateralManagement, fullLp } = fixtureResult; + + const resignTx = await collateralManagement.connect(fullLp).resign(); + await expect(resignTx) + .to.emit(collateralManagement, "Resigned") + .withArgs(fullLp.address); + + // Resigned provider must not appear in discovery list + const listed = await discovery.getProviders(); + expect(listed.some((p) => p.providerAddress === fullLp.address)).to.eq( + false + ); + }); + + it("prevent non-registered account from resigning", async () => { + const fixtureResult = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const { collateralManagement, signers } = fixtureResult; + const nonRegisteredAccount = signers[0]; + await expect( + collateralManagement.connect(nonRegisteredAccount).resign() + ).to.be.revertedWithCustomError( + collateralManagement, + "ProviderNotRegistered" + ); + }); + + it("prevent collateral withdrawal before delay and allow after", async () => { + const fixtureResult = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const { collateralManagement, pegInLp } = fixtureResult; + + const resignBlocks = await collateralManagement.getResignDelayInBlocks(); + + await expect( + collateralManagement.connect(pegInLp).withdrawCollateral() + ).to.be.revertedWithCustomError(collateralManagement, "NotResigned"); + await collateralManagement.connect(pegInLp).resign(); + await expect( + collateralManagement.connect(pegInLp).withdrawCollateral() + ).to.be.revertedWithCustomError( + collateralManagement, + "ResignationDelayNotMet" + ); + + await hardhatHelpers.mine(resignBlocks); + await expect(collateralManagement.connect(pegInLp).withdrawCollateral()).not + .to.be.reverted; + }); + + it("prevent double resign", async () => { + const fixtureResult = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const { collateralManagement, pegOutLp } = fixtureResult; + + await expect(collateralManagement.connect(pegOutLp).resign()).not.to.be + .reverted; + await expect( + collateralManagement.connect(pegOutLp).resign() + ).to.be.revertedWithCustomError(collateralManagement, "AlreadyResigned"); + }); + + describe("happy path (split contracts)", () => { + it("resign when LP is both pegin and pegout", async () => { + const fixtureResult = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const { collateralManagement, fullLp, MIN_COLLATERAL } = fixtureResult; + const collateral = MIN_COLLATERAL * 2n; // Both provider registers with 2x min collateral + + const resignBlocks = await collateralManagement.getResignDelayInBlocks(); + + const collateralBalanceAfterResignAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await collateralManagement.getAddress(), + expectedDiff: 0, + message: "Incorrect collateral management balance after resign", + }); + + const lpBalanceAfterCollateralWithdrawAssertion = + await createBalanceUpdateAssertion({ + source: ethers.provider, + address: fullLp.address, + message: "Incorrect LP balance after collateral withdraw", + }); + + const collateralBalanceAfterCollateralWithdrawAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await collateralManagement.getAddress(), + expectedDiff: collateral * -1n, + message: + "Incorrect collateral management balance after collateral withdraw", + }); + + const resignTx = await collateralManagement.connect(fullLp).resign(); + const resignReceipt = await resignTx.wait(); + await collateralBalanceAfterResignAssertion(); + + await expect(resignTx) + .to.emit(collateralManagement, "Resigned") + .withArgs(fullLp.address); + + await hardhatHelpers.mine(resignBlocks); + const withdrawCollateralTx = await collateralManagement + .connect(fullLp) + .withdrawCollateral(); + const withdrawCollateralReceipt = await withdrawCollateralTx.wait(); + await expect(withdrawCollateralTx) + .to.emit(collateralManagement, "WithdrawCollateral") + .withArgs(fullLp.address, collateral); + await lpBalanceAfterCollateralWithdrawAssertion( + collateral - withdrawCollateralReceipt!.fee - resignReceipt!.fee + ); + await collateralBalanceAfterCollateralWithdrawAssertion(); + await expect( + collateralManagement.getPegInCollateral(fullLp.address) + ).to.eventually.eq(0); + await expect( + collateralManagement.getPegOutCollateral(fullLp.address) + ).to.eventually.eq(0); + }); + + it("resign when LP is pegin only", async () => { + const fixtureResult = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const { collateralManagement, pegInLp, MIN_COLLATERAL } = fixtureResult; + const collateral = MIN_COLLATERAL; + + const resignBlocks = await collateralManagement.getResignDelayInBlocks(); + + const collateralBalanceAfterResignAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await collateralManagement.getAddress(), + expectedDiff: 0, + message: "Incorrect collateral management balance after resign", + }); + + const lpBalanceAfterCollateralWithdrawAssertion = + await createBalanceUpdateAssertion({ + source: ethers.provider, + address: pegInLp.address, + message: "Incorrect LP balance after collateral withdraw", + }); + + const collateralBalanceAfterCollateralWithdrawAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await collateralManagement.getAddress(), + expectedDiff: collateral * -1n, + message: + "Incorrect collateral management balance after collateral withdraw", + }); + + const resignTx = await collateralManagement.connect(pegInLp).resign(); + const resignReceipt = await resignTx.wait(); + await collateralBalanceAfterResignAssertion(); + + await expect(resignTx) + .to.emit(collateralManagement, "Resigned") + .withArgs(pegInLp.address); + + await hardhatHelpers.mine(resignBlocks); + const withdrawCollateralTx = await collateralManagement + .connect(pegInLp) + .withdrawCollateral(); + const withdrawCollateralReceipt = await withdrawCollateralTx.wait(); + await expect(withdrawCollateralTx) + .to.emit(collateralManagement, "WithdrawCollateral") + .withArgs(pegInLp.address, collateral); + await lpBalanceAfterCollateralWithdrawAssertion( + collateral - withdrawCollateralReceipt!.fee - resignReceipt!.fee + ); + await collateralBalanceAfterCollateralWithdrawAssertion(); + await expect( + collateralManagement.getPegInCollateral(pegInLp.address) + ).to.eventually.eq(0); + await expect( + collateralManagement.getPegOutCollateral(pegInLp.address) + ).to.eventually.eq(0); + }); + + it("resign when LP is pegout only", async () => { + const fixtureResult = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const { collateralManagement, pegOutLp, MIN_COLLATERAL } = fixtureResult; + const collateral = MIN_COLLATERAL; + + const resignBlocks = await collateralManagement.getResignDelayInBlocks(); + + const collateralBalanceAfterResignAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await collateralManagement.getAddress(), + expectedDiff: 0, + message: "Incorrect collateral management balance after resign", + }); + + const lpBalanceAfterCollateralWithdrawAssertion = + await createBalanceUpdateAssertion({ + source: ethers.provider, + address: pegOutLp.address, + message: "Incorrect LP balance after collateral withdraw", + }); + + const collateralBalanceAfterCollateralWithdrawAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await collateralManagement.getAddress(), + expectedDiff: collateral * -1n, + message: + "Incorrect collateral management balance after collateral withdraw", + }); + + const resignTx = await collateralManagement.connect(pegOutLp).resign(); + const resignReceipt = await resignTx.wait(); + await collateralBalanceAfterResignAssertion(); + await expect(resignTx) + .to.emit(collateralManagement, "Resigned") + .withArgs(pegOutLp.address); + + await hardhatHelpers.mine(resignBlocks); + const withdrawCollateralTx = await collateralManagement + .connect(pegOutLp) + .withdrawCollateral(); + const withdrawCollateralReceipt = await withdrawCollateralTx.wait(); + await expect(withdrawCollateralTx) + .to.emit(collateralManagement, "WithdrawCollateral") + .withArgs(pegOutLp.address, collateral); + await lpBalanceAfterCollateralWithdrawAssertion( + collateral - withdrawCollateralReceipt!.fee - resignReceipt!.fee + ); + await collateralBalanceAfterCollateralWithdrawAssertion(); + await expect( + collateralManagement.getPegInCollateral(pegOutLp.address) + ).to.eventually.eq(0); + await expect( + collateralManagement.getPegOutCollateral(pegOutLp.address) + ).to.eventually.eq(0); + }); + }); +}); diff --git a/test/discovery/status.test.ts b/test/discovery/status.test.ts new file mode 100644 index 00000000..6aae73bf --- /dev/null +++ b/test/discovery/status.test.ts @@ -0,0 +1,45 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { deployDiscoveryWithProvidersFixture } from "./fixtures"; + +describe("FlyoverDiscovery setProviderStatus", () => { + it("allows provider to disable and enable itself", async () => { + const { discovery, pegOutLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const connected = discovery.connect(pegOutLp); + + await connected.setProviderStatus(2, false); + let provider = await discovery.getProvider(pegOutLp.address); + expect(provider.status).to.equal(false); + + await connected.setProviderStatus(2, true); + provider = await discovery.getProvider(pegOutLp.address); + expect(provider.status).to.equal(true); + }); + + it("allows owner to toggle provider status", async () => { + const { discovery, owner, pegInLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const connected = discovery.connect(owner); + + await connected.setProviderStatus(1, false); + let provider = await discovery.getProvider(pegInLp.address); + expect(provider.status).to.equal(false); + + await connected.setProviderStatus(1, true); + provider = await discovery.getProvider(pegInLp.address); + expect(provider.status).to.equal(true); + }); + + it("reverts for unauthorized address", async () => { + const { discovery, signers } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const stranger = signers[0]; + await expect( + discovery.connect(stranger).setProviderStatus(1, false) + ).to.be.revertedWithCustomError(discovery, "NotAuthorized"); + }); +}); diff --git a/test/discovery/update.test.ts b/test/discovery/update.test.ts new file mode 100644 index 00000000..db693b67 --- /dev/null +++ b/test/discovery/update.test.ts @@ -0,0 +1,46 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { deployDiscoveryWithProvidersFixture } from "./fixtures"; + +describe("FlyoverDiscovery updateProvider", () => { + it("updates name and apiBaseUrl and emits event", async () => { + const { discovery, fullLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const connected = discovery.connect(fullLp); + + const newName = "Modified Name"; + const newUrl = "https://modified.example"; + + await expect(connected.updateProvider(newName, newUrl)) + .to.emit(discovery, "ProviderUpdate") + .withArgs(fullLp.address, newName, newUrl); + + const updated = await discovery.getProvider(fullLp.address); + expect(updated.name).to.equal(newName); + expect(updated.apiBaseUrl).to.equal(newUrl); + }); + + it("reverts on invalid input (empty name or url)", async () => { + const { discovery, fullLp } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const connected = discovery.connect(fullLp); + await expect( + connected.updateProvider("", "x") + ).to.be.revertedWithCustomError(discovery, "InvalidProviderData"); + await expect( + connected.updateProvider("x", "") + ).to.be.revertedWithCustomError(discovery, "InvalidProviderData"); + }); + + it("reverts if unregistered address calls update", async () => { + const { discovery, signers } = await loadFixture( + deployDiscoveryWithProvidersFixture + ); + const stranger = signers[0]; + await expect( + discovery.connect(stranger).updateProvider("n", "u") + ).to.be.revertedWithCustomError(discovery, "ProviderNotRegistered"); + }); +}); diff --git a/test/pegin.test.ts b/test/pegin.test.ts index f48ee032..a631fe60 100644 --- a/test/pegin.test.ts +++ b/test/pegin.test.ts @@ -547,7 +547,7 @@ describe("LiquidityBridgeContractV2 pegin process should", () => { productFeeAmount: BigInt("6000000000000000"), gasFee: BigInt("3000000000000000"), }, - address: "2N6qBRtB1i9QeCfU8U1K9CjY4G3z61wRbE3", + address: "2N9ZrZBxbCGr5ogRqhU4kf4i1cepD2EFxbF", }, { quote: { @@ -579,7 +579,7 @@ describe("LiquidityBridgeContractV2 pegin process should", () => { productFeeAmount: BigInt("7000000000000000"), gasFee: BigInt("4000000000000000"), }, - address: "2NBNaPEFCgFLGbJUcebWeXkE3utuNWwXTiX", + address: "2N1vNs9c2nREgEhvv3Byr1YbiJd44wTw7Q3", }, { quote: { @@ -611,7 +611,7 @@ describe("LiquidityBridgeContractV2 pegin process should", () => { productFeeAmount: BigInt("8000000000000000"), gasFee: BigInt("5000000000000000"), }, - address: "2N4HFNC6KzaZAg9zbibgsET6TyrfWA78CRx", + address: "2Mv5QotabZeRfmwbwJR1cmzugGkyapWdBhj", }, ]; diff --git a/test/pegin/derivation-address.test.ts b/test/pegin/derivation-address.test.ts index f0690036..04c842c9 100644 --- a/test/pegin/derivation-address.test.ts +++ b/test/pegin/derivation-address.test.ts @@ -17,7 +17,7 @@ describe("PegInContract validatePegInDepositAddress function should", () => { { quote: { fedBTCAddr: "3GQ87zLKyTygsRMZ1hfCHZSdBxujzKoCCU", - lbcAddr: "0xC9a43158891282A2B1475592D5719c001986Aaec", + lbcAddr: "0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB", lpRSKAddr: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddr: "1111111111111111111114oLvT2", rskRefundAddr: "0xaC31A4bEEdd7EC916b7a48A612230Cb85c1aaf56", @@ -37,13 +37,13 @@ describe("PegInContract validatePegInDepositAddress function should", () => { gasFee: 547377600000, productFeeAmount: 0, }, - mainnetAddress: "3NL4vLByNyjHEZrZpjgW9Wtk2TcLsJSZAn", - testnetAddress: "2NDtGz57zzSEdSMV7VsJNmTt1EopWdjF8ty", + mainnetAddress: "3EgPC2vQZFx9bTNbYrVKD1Sz5F1ap36zUL", + testnetAddress: "2N6EbFmrSAiTVoF19Dz7BpxSFHbDkbox9DU", }, { quote: { fedBTCAddr: "3GQ87zLKyTygsRMZ1hfCHZSdBxujzKoCCU", - lbcAddr: "0xC9a43158891282A2B1475592D5719c001986Aaec", + lbcAddr: "0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB", lpRSKAddr: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddr: "1111111111111111111114oLvT2", rskRefundAddr: "0x129D2280f9c35c0cAf3f172D487fD9A3f894fD26", @@ -63,13 +63,13 @@ describe("PegInContract validatePegInDepositAddress function should", () => { gasFee: 547377600000, productFeeAmount: 0, }, - mainnetAddress: "3NYC8YUBenys3rF3XLGd3riBzVDcsGvnA9", - testnetAddress: "2NE6QCHQDGFVDFdsbCTtVfohTCqRngyKtz7", + mainnetAddress: "35b9LZnVxjbSJ42UnGqk2rp1QBtEn7TRCe", + testnetAddress: "2Mw9MQJiXaC6nVqf2TQTceooGcY6QbBFwwj", }, { quote: { fedBTCAddr: "3GQ87zLKyTygsRMZ1hfCHZSdBxujzKoCCU", - lbcAddr: "0xC9a43158891282A2B1475592D5719c001986Aaec", + lbcAddr: "0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB", lpRSKAddr: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddr: "1111111111111111111114oLvT2", rskRefundAddr: "0xaC31A4bEEdd7EC916b7a48A612230Cb85c1aaf56", @@ -89,8 +89,8 @@ describe("PegInContract validatePegInDepositAddress function should", () => { gasFee: 547377600000, productFeeAmount: 0, }, - mainnetAddress: "34W24FRzEwbW1yiqjs1QgtaXp2rbdboypb", - testnetAddress: "2Mv4E7zN1rQ6rDmMPQzdHJqZo2P4mRsc6dU", + mainnetAddress: "3CmBgsmJFEedx1LRiifoVBKA7Z1QrX8DD2", + testnetAddress: "2N4KPkchKrh9z9nxyPrHg78JRKuDagJJ5fc", }, ]; diff --git a/test/pegin/hashing.test.ts b/test/pegin/hashing.test.ts index 71e8f300..9ce89991 100644 --- a/test/pegin/hashing.test.ts +++ b/test/pegin/hashing.test.ts @@ -10,7 +10,7 @@ describe("PegInContract hashPegInQuote function should", () => { "bc1quhhaa58r2xg3yu7ms85stpds0dmg896auw4nmh"; const QUOTE_MOCK: ApiPeginQuote = { fedBTCAddr: "3GQ87zLKyTygsRMZ1hfCHZSdBxujzKoCCU", - lbcAddr: "0x172076E0166D1F9Cc711C77Adf8488051744980C", + lbcAddr: "0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2", lpRSKAddr: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddr: "1111111111111111111114oLvT2", rskRefundAddr: "0xaC31A4bEEdd7EC916b7a48A612230Cb85c1aaf56", @@ -106,12 +106,12 @@ describe("PegInContract hashPegInQuote function should", () => { const testCases: { quote: ApiPeginQuote; hash: string }[] = [ { quote: QUOTE_MOCK, - hash: "0xe8b928c88de9e620e6f08645ed1b413bf05d9cbdcd23bd4bac980b8e6b041aad", + hash: "0x67e68a14a4a1ed6300970c7cd532cfd558206b3d7ac3fbc10e4cd67e5816e39d", }, { quote: { fedBTCAddr: "3GQ87zLKyTygsRMZ1hfCHZSdBxujzKoCCU", - lbcAddr: "0x172076E0166D1F9Cc711C77Adf8488051744980C", + lbcAddr: "0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2", lpRSKAddr: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddr: "1111111111111111111114oLvT2", rskRefundAddr: "0x129D2280f9c35c0cAf3f172D487fD9A3f894fD26", @@ -131,12 +131,12 @@ describe("PegInContract hashPegInQuote function should", () => { gasFee: 547377600000, productFeeAmount: 0, }, - hash: "0x786f5080f538683f02952ed2aab27417bcd9453911067b510a83dee9d0430943", + hash: "0xf346bb77750943dbc7753f67ed01bd6c2be3ac77625157147687ccb4db72b3f7", }, { quote: { fedBTCAddr: "3GQ87zLKyTygsRMZ1hfCHZSdBxujzKoCCU", - lbcAddr: "0x172076E0166D1F9Cc711C77Adf8488051744980C", + lbcAddr: "0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2", lpRSKAddr: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddr: "1111111111111111111114oLvT2", rskRefundAddr: "0xaC31A4bEEdd7EC916b7a48A612230Cb85c1aaf56", @@ -156,7 +156,7 @@ describe("PegInContract hashPegInQuote function should", () => { gasFee: 547377600000, productFeeAmount: 0, }, - hash: "0x75b60cfb94c8cf1128f469f0ff43e0869a0e2ac85f62f696e2a1256b657c2017", + hash: "0x727ecad37c5569ebf89dcd67d76d0d60f2ea0dcf4efdf7ad6e061352ae0a85cc", }, ]; for (const testCase of testCases) { diff --git a/test/pegout/deposit.test.ts b/test/pegout/deposit.test.ts index d695beea..a7842915 100644 --- a/test/pegout/deposit.test.ts +++ b/test/pegout/deposit.test.ts @@ -281,7 +281,7 @@ describe("PegOutContract depositPegOut function should", () => { .depositPegOut(quote, signature, { value: paidAmount }); await expect(tx) .to.emit(contract, "PegOutDeposit") - .withArgs(quoteHash, user.address, paidAmount, matchAnyNumber); + .withArgs(quoteHash, user.address, matchAnyNumber, paidAmount); await expect(tx).not.to.emit(contract, "PegOutChangePaid"); await expect(tx).to.changeEtherBalances( [user, contract], @@ -315,7 +315,7 @@ describe("PegOutContract depositPegOut function should", () => { .depositPegOut(quote, signature, { value: paidAmount }); await expect(tx) .to.emit(contract, "PegOutDeposit") - .withArgs(quoteHash, user.address, paidAmount, matchAnyNumber); + .withArgs(quoteHash, user.address, matchAnyNumber, paidAmount); await expect(tx) .to.emit(contract, "PegOutChangePaid") .withArgs(quoteHash, user.address, changeAmount); diff --git a/test/pegout/hashing.test.ts b/test/pegout/hashing.test.ts index 78b66e3e..d9527d68 100644 --- a/test/pegout/hashing.test.ts +++ b/test/pegout/hashing.test.ts @@ -36,7 +36,7 @@ describe("PegOutContract hashPegOutQuote function should", () => { const testCases: { quote: ApiPegoutQuote; hash: string }[] = [ { quote: { - lbcAddress: "0xD84379CEae14AA33C123Af12424A37803F885889", + lbcAddress: "0x51A1ceB83B83F1985a81C295d1fF28Afef186E02", liquidityProviderRskAddress: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddress: "bc1qlc98wwylr3g6kknh86a8gkdqmhf6vly527h2yv", @@ -57,11 +57,11 @@ describe("PegOutContract hashPegOutQuote function should", () => { gasFee: 5990000000000, productFeeAmount: 0, }, - hash: "0x71f7e479c6ba024ffd6faec5a9a8011370298a0269b3d01c0589a7827b8b528c", + hash: "0x65cdc2fa61131a201c9fb50aa54e5ef00c0381367bc466bf692db23941ba7020", }, { quote: { - lbcAddress: "0xD84379CEae14AA33C123Af12424A37803F885889", + lbcAddress: "0x51A1ceB83B83F1985a81C295d1fF28Afef186E02", liquidityProviderRskAddress: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddress: "1KMCKD5ySjvugtyBgiADNhvDJ42QRD9Erp", @@ -82,11 +82,11 @@ describe("PegOutContract hashPegOutQuote function should", () => { gasFee: 11330000000000, productFeeAmount: 1, }, - hash: "0x8e26cd6350bb97496c8ddb813f68c3d977f3c426327a8b3847c143c5bbb53960", + hash: "0x5abcfb321ca73510f574609aaafb0bc560fdca7630f5a19bcb054d63b81911c9", }, { quote: { - lbcAddress: "0xD84379CEae14AA33C123Af12424A37803F885889", + lbcAddress: "0x51A1ceB83B83F1985a81C295d1fF28Afef186E02", liquidityProviderRskAddress: "0x82a06ebdb97776a2da4041df8f2b2ea8d3257852", btcRefundAddress: @@ -109,7 +109,7 @@ describe("PegOutContract hashPegOutQuote function should", () => { gasFee: 3140000000000, productFeeAmount: 3, }, - hash: "0x0fadf6762870f343986a5817b8b7c522ac7ddfb2bb48f44f8c757d1b4a136dce", + hash: "0x89f7b303996fdfd4adc31681c6227fc2ecbdb850f7c9974717fc14cefd7f2b1e", }, ]; for (const testCase of testCases) { diff --git a/test/utils/fixtures.ts b/test/utils/fixtures.ts index 413bb75a..86cf82d0 100644 --- a/test/utils/fixtures.ts +++ b/test/utils/fixtures.ts @@ -9,12 +9,11 @@ import { deployCollateralManagement } from "../collateral/fixtures"; export async function deployCollateralManagementAndDiscovery() { const { collateralManagement, signers, owner } = await deployCollateralManagement(); - const FlyoverDiscovery = await ethers.getContractFactory( - "FlyoverDiscoveryContract" - ); + const FlyoverDiscovery = await ethers.getContractFactory("FlyoverDiscovery"); const discovery = await upgrades.deployProxy(FlyoverDiscovery, [ owner.address, + 5000n, await collateralManagement.getAddress(), ]); await collateralManagement