diff --git a/contracts/apx-protocol/ApxAssetsRegistry.sol b/contracts/apx-protocol/ApxAssetsRegistry.sol index 4a29c6f..8e7261d 100644 --- a/contracts/apx-protocol/ApxAssetsRegistry.sol +++ b/contracts/apx-protocol/ApxAssetsRegistry.sol @@ -26,7 +26,14 @@ contract ApxAssetsRegistry is IApxAssetsRegistry { // EVENTS //------------------------ event RegisterAsset(address caller, address original, address mirrored, bool state, uint256 timestamp); - event UpdatePrice(address priceManager, address asset, uint256 price, uint256 expiry, uint256 timestamp); + event UpdatePrice( + address priceManager, + address asset, + uint256 price, + uint8 priceDecimal, + uint256 expiry, + uint256 timestamp + ); event UpdateState(address assetManager, address asset, bool active, uint256 timestamp); event TransferMasterOwnerRole(address oldMasterOwner, address newMasterOwner, uint256 timestamp); event TransferAssetManagerRole(address oldAssetManager, address newAssetManager, uint256 timestamp); @@ -110,7 +117,7 @@ contract ApxAssetsRegistry is IApxAssetsRegistry { true, state, block.timestamp, - 0, 0, 0, 0, address(0) + 0, 0, 0, 0, 0, address(0) ); assetsList.push(mirrored); emit RegisterAsset(msg.sender, original, mirrored, state, block.timestamp); @@ -128,6 +135,7 @@ contract ApxAssetsRegistry is IApxAssetsRegistry { function updatePrice( address asset, uint256 price, + uint8 priceDecimals, uint256 expiry, uint256 capturedSupply ) external override onlyPriceManagerOrMasterOwner assetExists(asset) { @@ -136,11 +144,12 @@ contract ApxAssetsRegistry is IApxAssetsRegistry { require(expiry > 0, "ApxAssetsRegistry: expiry has to be > 0;"); require(capturedSupply == IToken(asset).totalSupply(), "ApxAssetsRegistry: inconsistent asset supply."); assets[asset].price = price; + assets[asset].priceDecimals = priceDecimals; assets[asset].priceUpdatedAt = block.timestamp; assets[asset].priceValidUntil = block.timestamp + expiry; assets[asset].capturedSupply = capturedSupply; assets[asset].priceProvider = msg.sender; - emit UpdatePrice(msg.sender, asset, price, expiry, block.timestamp); + emit UpdatePrice(msg.sender, asset, price, priceDecimals, expiry, block.timestamp); } function migrate(address newAssetsRegistry, address originalAsset) external override onlyMasterOwner { diff --git a/contracts/apx-protocol/IApxAssetsRegistry.sol b/contracts/apx-protocol/IApxAssetsRegistry.sol index 83536ff..e8be890 100644 --- a/contracts/apx-protocol/IApxAssetsRegistry.sol +++ b/contracts/apx-protocol/IApxAssetsRegistry.sol @@ -15,6 +15,7 @@ interface IApxAssetsRegistry is IVersioned { function updatePrice( address asset, uint256 price, + uint8 priceDecimals, uint256 expiry, uint256 capturedSupply ) external; diff --git a/contracts/asset-simple/AssetSimple.sol b/contracts/asset-simple/AssetSimple.sol index 8240cc2..46e472b 100644 --- a/contracts/asset-simple/AssetSimple.sol +++ b/contracts/asset-simple/AssetSimple.sol @@ -11,11 +11,6 @@ import "../shared/ICampaignCommon.sol"; contract AssetSimple is IAssetSimple, ERC20 { - //------------------------ - // CONSTANTS - //------------------------ - uint256 constant public override priceDecimalsPrecision = 10 ** 4; - //----------------------- // STATE //----------------------- diff --git a/contracts/asset-transferable/AssetTransferable.sol b/contracts/asset-transferable/AssetTransferable.sol index 7fcd24a..3ae2102 100644 --- a/contracts/asset-transferable/AssetTransferable.sol +++ b/contracts/asset-transferable/AssetTransferable.sol @@ -13,11 +13,6 @@ import "../shared/Structs.sol"; contract AssetTransferable is IAssetTransferable, ERC20 { using SafeERC20 for IERC20; - //------------------------ - // CONSTANTS - //------------------------ - uint256 constant public override priceDecimalsPrecision = 10 ** 4; - //---------------------- // STATE //------------------------ @@ -72,7 +67,7 @@ contract AssetTransferable is IAssetTransferable, ERC20 { params.info, params.name, params.symbol, - 0, 0, 0, + 0, 0, 0, 0, false, 0, 0, 0 ); @@ -149,6 +144,7 @@ contract AssetTransferable is IAssetTransferable, ERC20 { uint256 tokenValue = campaignState.fundsRaised; uint256 tokenAmount = campaignState.tokensSold; uint256 tokenPrice = campaignState.pricePerToken; + uint8 tokenPriceDecimals = campaignState.tokenPriceDecimals; require( tokenAmount > 0 && balanceOf(campaign) >= tokenAmount, "AssetTransferable: Campaign has signalled the sale finalization but campaign tokens are not present" @@ -164,7 +160,14 @@ contract AssetTransferable is IAssetTransferable, ERC20 { ); sellHistory.push(tokenSaleInfo); successfulTokenSalesMap[campaign] = tokenSaleInfo; - if (tokenPrice > state.highestTokenSellPrice) { state.highestTokenSellPrice = tokenPrice; } + if ( + (state.highestTokenSellPrice == 0 && state.highestTokenSellPriceDecimals == 0) || + _tokenValue(tokenAmount, tokenPrice, tokenPriceDecimals) > + _tokenValue(tokenAmount, state.highestTokenSellPrice, state.highestTokenSellPriceDecimals) + ) { + state.highestTokenSellPrice = tokenPrice; + state.highestTokenSellPriceDecimals = tokenPriceDecimals; + } emit FinalizeSale( msg.sender, tokenAmount, @@ -179,17 +182,47 @@ contract AssetTransferable is IAssetTransferable, ERC20 { require(assetRecord.exists, "AssetTransferable: Not registered in Apx Registry"); require(assetRecord.state, "AssetTransferable: Asset blocked in Apx Registry"); require(assetRecord.mirroredToken == address(this), "AssetTransferable: Invalid mirrored asset record"); - require(block.timestamp <= assetRecord.priceValidUntil, "AssetTransferable: Price expired"); - uint256 liquidationPrice = - (state.highestTokenSellPrice > assetRecord.price) ? state.highestTokenSellPrice : assetRecord.price; + require(block.timestamp <= assetRecord.priceValidUntil, "AssetTransferable: Price expired"); + uint256 liquidationAmount; + uint256 liquidationPrice; + uint8 liquidationPriceDecimals; + uint256 liquidationAmountPerLargestSalePrice = _tokenValue( + totalSupply(), + state.highestTokenSellPrice, + state.highestTokenSellPriceDecimals + ); + uint256 liquidationAmountPerMarketPrice = _tokenValue( + totalSupply(), + assetRecord.price, + assetRecord.priceDecimals + ); + if (liquidationAmountPerLargestSalePrice > liquidationAmountPerMarketPrice) { + liquidationAmount = liquidationAmountPerLargestSalePrice; + liquidationPrice = state.highestTokenSellPrice; + liquidationPriceDecimals = state.highestTokenSellPriceDecimals; + } else { + liquidationAmount = liquidationAmountPerMarketPrice; + liquidationPrice = assetRecord.price; + liquidationPriceDecimals = assetRecord.priceDecimals; + } + uint256 liquidatorApprovedTokenAmount = this.allowance(msg.sender, address(this)); - uint256 liquidatorApprovedTokenValue = _tokenValue(liquidatorApprovedTokenAmount, liquidationPrice); + uint256 liquidatorApprovedTokenValue = _tokenValue( + liquidatorApprovedTokenAmount, + liquidationPrice, + liquidationPriceDecimals + ); + if (liquidatorApprovedTokenValue > 0) { liquidationClaimsMap[msg.sender] += liquidatorApprovedTokenValue; state.liquidationFundsClaimed += liquidatorApprovedTokenValue; this.transferFrom(msg.sender, address(this), liquidatorApprovedTokenAmount); } - uint256 liquidationFundsTotal = _tokenValue(totalSupply(), liquidationPrice); + uint256 liquidationFundsTotal = _tokenValue( + totalSupply(), + liquidationPrice, + liquidationPriceDecimals + ); uint256 liquidationFundsToPull = liquidationFundsTotal - liquidatorApprovedTokenValue; if (liquidationFundsToPull > 0) { _stablecoin().safeTransferFrom(msg.sender, address(this), liquidationFundsToPull); @@ -308,19 +341,16 @@ contract AssetTransferable is IAssetTransferable, ERC20 { return true; } - function _tokenValue(uint256 amount, uint256 price) private view returns (uint256) { + function _tokenValue(uint256 amount, uint256 price, uint8 priceDecimals) private view returns (uint256) { return amount * price * _stablecoin_decimals_precision() - / (_asset_decimals_precision() * priceDecimalsPrecision); + / (10 ** priceDecimals) + / (10 ** decimals()); } function _stablecoin_decimals_precision() private view returns (uint256) { return 10 ** IToken(_stablecoin_address()).decimals(); } - function _asset_decimals_precision() private view returns (uint256) { - return 10 ** decimals(); - } - } diff --git a/contracts/asset/Asset.sol b/contracts/asset/Asset.sol index 9344f8d..bd78308 100644 --- a/contracts/asset/Asset.sol +++ b/contracts/asset/Asset.sol @@ -14,11 +14,6 @@ import "../shared/Structs.sol"; contract Asset is IAsset, ERC20 { using SafeERC20 for IERC20; - //------------------------ - // CONSTANTS - //------------------------ - uint256 constant public override priceDecimalsPrecision = 10 ** 4; - //----------------------- // STATE //----------------------- @@ -69,7 +64,7 @@ contract Asset is IAsset, ERC20 { params.info, params.name, params.symbol, - 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, false, 0, 0, 0 ); @@ -183,6 +178,7 @@ contract Asset is IAsset, ERC20 { uint256 tokenValue = campaignState.fundsRaised; uint256 tokenAmount = campaignState.tokensSold; uint256 tokenPrice = campaignState.pricePerToken; + uint8 tokenPriceDecimals = campaignState.tokenPriceDecimals; require( tokenAmount > 0 && balanceOf(campaign) >= tokenAmount, "Asset: Campaign has signalled the sale finalization but campaign tokens are not present" @@ -198,7 +194,14 @@ contract Asset is IAsset, ERC20 { ); sellHistory.push(tokenSaleInfo); successfulTokenSalesMap[campaign] = tokenSaleInfo; - if (tokenPrice > state.highestTokenSellPrice) { state.highestTokenSellPrice = tokenPrice; } + if ( + (state.highestTokenSellPrice == 0 && state.highestTokenSellPriceDecimals == 0) || + _tokenValue(tokenAmount, tokenPrice, tokenPriceDecimals) > + _tokenValue(tokenAmount, state.highestTokenSellPrice, state.highestTokenSellPriceDecimals) + ) { + state.highestTokenSellPrice = tokenPrice; + state.highestTokenSellPriceDecimals = tokenPriceDecimals; + } emit FinalizeSale( msg.sender, tokenAmount, @@ -209,7 +212,14 @@ contract Asset is IAsset, ERC20 { function liquidate() external override ownerOnly { require(!state.liquidated, "Asset: Action forbidden, asset liquidated."); + uint256 liquidationAmount; uint256 liquidationPrice; + uint8 liquidationPriceDecimals; + uint256 liquidationAmountPerLargestSalePrice = _tokenValue( + totalSupply(), + state.highestTokenSellPrice, + state.highestTokenSellPriceDecimals + ); if (state.totalTokensLocked > 0) { IApxAssetsRegistry apxRegistry = IApxAssetsRegistry(state.apxRegistry); Structs.AssetRecord memory assetRecord = apxRegistry.getMirroredFromOriginal(address(this)); @@ -217,21 +227,43 @@ contract Asset is IAsset, ERC20 { require(assetRecord.originalToken == address(this), "Asset: Invalid mirrored asset record"); require(block.timestamp <= assetRecord.priceValidUntil, "Asset: Price expired"); require(state.totalTokensLocked == assetRecord.capturedSupply, "Asset: MirroredToken supply inconsistent"); - liquidationPrice = - (state.highestTokenSellPrice > assetRecord.price) ? state.highestTokenSellPrice : assetRecord.price; + uint256 liquidationAmountPerMarketPrice = _tokenValue( + totalSupply(), + assetRecord.price, + assetRecord.priceDecimals + ); + if (liquidationAmountPerLargestSalePrice > liquidationAmountPerMarketPrice) { + liquidationAmount = liquidationAmountPerLargestSalePrice; + liquidationPrice = state.highestTokenSellPrice; + liquidationPriceDecimals = state.highestTokenSellPriceDecimals; + } else { + liquidationAmount = liquidationAmountPerMarketPrice; + liquidationPrice = assetRecord.price; + liquidationPriceDecimals = assetRecord.priceDecimals; + } } else { + liquidationAmount = liquidationAmountPerLargestSalePrice; liquidationPrice = state.highestTokenSellPrice; + liquidationPriceDecimals = state.highestTokenSellPriceDecimals; } uint256 liquidatorApprovedTokenAmount = this.allowance(msg.sender, address(this)); - uint256 liquidatorApprovedTokenValue = _tokenValue(liquidatorApprovedTokenAmount, liquidationPrice); + uint256 liquidatorApprovedTokenValue = _tokenValue( + liquidatorApprovedTokenAmount, + liquidationPrice, + liquidationPriceDecimals + ); if (liquidatorApprovedTokenValue > 0) { liquidationClaimsMap[msg.sender] += liquidatorApprovedTokenValue; state.liquidationFundsClaimed += liquidatorApprovedTokenValue; this.transferFrom(msg.sender, address(this), liquidatorApprovedTokenAmount); } - uint256 liquidationFundsTotal = _tokenValue(totalSupply(), liquidationPrice); + uint256 liquidationFundsTotal = _tokenValue( + totalSupply(), + liquidationPrice, + liquidationPriceDecimals + ); uint256 liquidationFundsToPull = liquidationFundsTotal - liquidatorApprovedTokenValue; if (liquidationFundsToPull > 0) { _stablecoin().safeTransferFrom(msg.sender, address(this), liquidationFundsToPull); @@ -348,11 +380,12 @@ contract Asset is IAsset, ERC20 { return (campaignRecord.wallet == campaignAddress && campaignRecord.whitelisted); } - function _tokenValue(uint256 amount, uint256 price) private view returns (uint256) { + function _tokenValue(uint256 amount, uint256 price, uint8 priceDecimals) private view returns (uint256) { return amount * price * (10 ** IToken(_stablecoin_address()).decimals()) - / ((10 ** decimals()) * priceDecimalsPrecision); + / (10 ** priceDecimals) + / (10 ** decimals()); } } diff --git a/contracts/issuer/IIssuer.sol b/contracts/issuer/IIssuer.sol index 172cfc1..f4e1ef1 100644 --- a/contracts/issuer/IIssuer.sol +++ b/contracts/issuer/IIssuer.sol @@ -6,10 +6,6 @@ import "../shared/Structs.sol"; interface IIssuer is IIssuerCommon { - // Write - - function changeOwnership(address newOwner) external; - // Read function getState() external view returns (Structs.IssuerState memory); diff --git a/contracts/issuer/Issuer.sol b/contracts/issuer/Issuer.sol index a94bafb..61993ff 100644 --- a/contracts/issuer/Issuer.sol +++ b/contracts/issuer/Issuer.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import "@openzeppelin/contracts/access/Ownable.sol"; import "../issuer/IIssuer.sol"; import "../shared/Structs.sol"; -contract Issuer is IIssuer { +contract Issuer is IIssuer, Ownable { //------------------------ // STATE @@ -19,7 +20,6 @@ contract Issuer is IIssuer { //------------------------ event WalletWhitelist(address indexed approver, address indexed wallet); event WalletBlacklist(address indexed approver, address indexed wallet); - event ChangeOwnership(address caller, address newOwner, uint256 timestamp); event ChangeWalletApprover(address caller, address oldWalletApprover, address newWalletApprover, uint256 timestamp); event SetInfo(string info, address setter, uint256 timestamp); @@ -35,7 +35,6 @@ contract Issuer is IIssuer { string memory info ) { require(owner != address(0), "Issuer: invalid owner address"); - require(stablecoin != address(0), "Issuer: invalid stablecoin address"); require(walletApprover != address(0), "Issuer: invalid wallet approver address"); infoHistory.push(Structs.InfoEntry( @@ -52,19 +51,12 @@ contract Issuer is IIssuer { info ); _setWalletState(owner, true); + _transferOwnership(owner); } //------------------------ // MODIFIERS //------------------------ - modifier ownerOnly { - require( - msg.sender == state.owner, - "Issuer: Only owner can make this action." - ); - _; - } - modifier walletApproverOnly { require( msg.sender == state.walletApprover, @@ -76,7 +68,7 @@ contract Issuer is IIssuer { //------------------------ // IIssuer IMPL //------------------------ - function setInfo(string memory info) external override ownerOnly { + function setInfo(string memory info) external override onlyOwner { infoHistory.push(Structs.InfoEntry( info, block.timestamp @@ -95,9 +87,9 @@ contract Issuer is IIssuer { emit WalletBlacklist(msg.sender, wallet); } - function changeOwnership(address newOwner) external override ownerOnly { + function transferOwnership(address newOwner) public override onlyOwner { + super.transferOwnership(newOwner); state.owner = newOwner; - emit ChangeOwnership(msg.sender, newOwner, block.timestamp); } function changeWalletApprover(address newWalletApprover) external override { diff --git a/contracts/managers/ACfManager.sol b/contracts/managers/ACfManager.sol index a6ff0f7..5ec802d 100644 --- a/contracts/managers/ACfManager.sol +++ b/contracts/managers/ACfManager.sol @@ -3,13 +3,14 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; import "../shared/Structs.sol"; import "../tokens/erc20/IToken.sol"; import "../shared/IAssetCommon.sol"; import "../shared/IIssuerCommon.sol"; import "./IACfManager.sol"; -abstract contract ACfManager is IVersioned, IACfManager { +abstract contract ACfManager is IVersioned, IACfManager, Ownable { using SafeERC20 for IERC20; //------------------------ @@ -55,19 +56,10 @@ abstract contract ACfManager is IVersioned, IACfManager { ); event CancelCampaign(address indexed owner, address asset, uint256 tokensReturned, uint256 timestamp); event SetInfo(string info, address setter, uint256 timestamp); - event ChangeOwnership(address caller, address newOwner, uint256 timestamp); //------------------------ // MODIFIERS //------------------------ - modifier ownerOnly() { - require( - msg.sender == state.owner, - "ACfManager: Only owner can call this function." - ); - _; - } - modifier active() { require( !state.canceled, @@ -129,7 +121,7 @@ abstract contract ACfManager is IVersioned, IACfManager { _cancel_investment(investor); } - function finalize() external ownerOnly active notFinalized { + function finalize() external onlyOwner active notFinalized { IERC20 sc = stablecoin(); uint256 fundsRaised = sc.balanceOf(address(this)); require( @@ -140,21 +132,13 @@ abstract contract ACfManager is IVersioned, IACfManager { IERC20 assetERC20 = _assetERC20(); uint256 tokensSold = state.totalTokensSold; uint256 tokensRefund = assetERC20.balanceOf(address(this)) - tokensSold; - IAssetCommon(state.asset).finalizeSale(); - if (fundsRaised > 0) { - (address treasury, uint256 fee) = _calculateFee(); - if (fee > 0 && treasury != address(0)) { - sc.safeTransfer(treasury, fee); - sc.safeTransfer(msg.sender, fundsRaised - fee); - } else { - sc.safeTransfer(msg.sender, fundsRaised); - } - } + _safeFinalizeSale(); + _safeDistributeFunds(msg.sender, fundsRaised, sc); if (tokensRefund > 0) { assetERC20.safeTransfer(msg.sender, tokensRefund); } emit Finalize(msg.sender, state.asset, fundsRaised, tokensSold, tokensRefund, block.timestamp); } - function cancelCampaign() external ownerOnly active notFinalized { + function cancelCampaign() external onlyOwner active notFinalized { state.canceled = true; uint256 tokenBalance = _assetERC20().balanceOf(address(this)); if(tokenBalance > 0) { _assetERC20().safeTransfer(msg.sender, tokenBalance); } @@ -170,7 +154,7 @@ abstract contract ACfManager is IVersioned, IACfManager { state.flavor, state.version, state.contractAddress, - state.owner, + owner(), state.info, state.asset, state.stablecoin, @@ -178,6 +162,7 @@ abstract contract ACfManager is IVersioned, IACfManager { state.finalized, state.canceled, state.tokenPrice, + state.tokenPriceDecimals, state.totalFundsRaised, state.totalTokensSold ); @@ -188,7 +173,7 @@ abstract contract ACfManager is IVersioned, IACfManager { function tokenAmount(address investor) external view override returns (uint256) { return tokenAmounts[investor]; } function claimedAmount(address investor) external view override returns (uint256) { return claims[investor]; } - function setInfo(string memory info) external override ownerOnly { + function setInfo(string memory info) external override onlyOwner { infoHistory.push(Structs.InfoEntry( info, block.timestamp @@ -201,11 +186,6 @@ abstract contract ACfManager is IVersioned, IACfManager { return infoHistory; } - function changeOwnership(address newOwner) external override ownerOnly { - state.owner = newOwner; - emit ChangeOwnership(msg.sender, newOwner, block.timestamp); - } - function isWalletWhitelisted(address wallet) public view returns (bool) { return !state.whitelistRequired || (state.whitelistRequired && _walletApproved(wallet)); } @@ -221,16 +201,40 @@ abstract contract ACfManager is IVersioned, IACfManager { require(amount > 0, "ACfManager: Investment amount has to be greater than 0."); uint256 tokenBalance = _assetERC20().balanceOf(address(this)); require( - _token_value(tokenBalance, state.tokenPrice, state.asset) >= state.softCap, + _token_value( + tokenBalance, + state.tokenPrice, + state.tokenPriceDecimals, + state.asset, + state.stablecoin + ) >= state.softCap, "ACfManager: not enough tokens for sale to reach the softcap." ); uint256 floatingTokens = tokenBalance - state.totalClaimableTokens; - uint256 tokens = _token_amount_for_investment(amount, state.tokenPrice, state.asset); - uint256 tokenValue = _token_value(tokens, state.tokenPrice, state.asset); + uint256 tokens = _token_amount_for_investment( + amount, + state.tokenPrice, + state.tokenPriceDecimals, + state.asset, + state.stablecoin + ); + uint256 tokenValue = _token_value( + tokens, + state.tokenPrice, + state.tokenPriceDecimals, + state.asset, + state.stablecoin + ); require(tokens > 0 && tokenValue > 0, "ACfManager: Investment amount too low."); require(floatingTokens >= tokens, "ACfManager: Not enough tokens left for this investment amount."); - uint256 totalInvestmentValue = _token_value(tokens + claims[investor], state.tokenPrice, state.asset); + uint256 totalInvestmentValue = _token_value( + tokens + claims[investor], + state.tokenPrice, + state.tokenPriceDecimals, + state.asset, + state.stablecoin + ); require( totalInvestmentValue >= _adjusted_min_investment(floatingTokens), "ACfManager: Investment amount too low." @@ -272,6 +276,24 @@ abstract contract ACfManager is IVersioned, IACfManager { emit CancelInvestment(investor, state.asset, tokens, tokenValue, block.timestamp); } + function _safeFinalizeSale() internal { + state.asset.call( + abi.encodeWithSignature("finalizeSale()") + ); + } + + function _safeDistributeFunds(address fundsDestination, uint256 fundsRaised, IERC20 sc) internal { + if (fundsRaised > 0) { + (address treasury, uint256 fee) = _calculateFee(); + if (fee > 0 && treasury != address(0)) { + sc.safeTransfer(treasury, fee); + sc.safeTransfer(fundsDestination, fundsRaised - fee); + } else { + sc.safeTransfer(fundsDestination, fundsRaised); + } + } + } + function _calculateFee() internal returns (address, uint256) { (bool success, bytes memory result) = state.feeManager.call( abi.encodeWithSignature("calculateFee(address)", address(this)) @@ -289,20 +311,21 @@ abstract contract ACfManager is IVersioned, IACfManager { return 10 ** IToken(asset).decimals(); } - function _asset_price_precision(address asset) internal view returns (uint256) { - return IAssetCommon(asset).priceDecimalsPrecision(); - } - function _stablecoin_decimals_precision(address stable) internal view returns (uint256) { return 10 ** IToken(stable).decimals(); } - function _token_value(uint256 tokens, uint256 tokenPrice, address asset) internal view returns (uint256) { - address stable = IIssuerCommon(IAssetCommon(asset).commonState().issuer).commonState().stablecoin; + function _token_value( + uint256 tokens, + uint256 tokenPrice, + uint8 tokenPriceDecimals, + address asset, + address stable + ) internal view returns (uint256) { return tokens * tokenPrice * _stablecoin_decimals_precision(stable) - / _asset_price_precision(asset) + / (10 ** tokenPriceDecimals) / _asset_decimals_precision(asset); } @@ -311,7 +334,13 @@ abstract contract ACfManager is IVersioned, IACfManager { } function _adjusted_min_investment(uint256 remainingTokens) internal view returns (uint256) { - uint256 remainingTokensValue = _token_value(remainingTokens, state.tokenPrice, state.asset); + uint256 remainingTokensValue = _token_value( + remainingTokens, + state.tokenPrice, + state.tokenPriceDecimals, + state.asset, + state.stablecoin + ); return (remainingTokensValue < state.minInvestment) ? remainingTokensValue : state.minInvestment; } @@ -319,21 +348,40 @@ abstract contract ACfManager is IVersioned, IACfManager { uint256 tokenAmountForInvestment = _token_amount_for_investment( state.softCap - state.totalFundsRaised, state.tokenPrice, - state.asset + state.tokenPriceDecimals, + state.asset, + state.stablecoin + ); + return _token_value( + tokenAmountForInvestment, + state.tokenPrice, + state.tokenPriceDecimals, + state.asset, + state.stablecoin ); - return _token_value(tokenAmountForInvestment, state.tokenPrice, state.asset); } function _token_amount_for_investment( uint256 investment, uint256 tokenPrice, - address asset + uint8 tokenPriceDecimals, + address asset, + address stable ) internal view returns (uint256) { - address stable = IIssuerCommon(IAssetCommon(asset).commonState().issuer).commonState().stablecoin; return investment - * _asset_price_precision(asset) + * (10 ** tokenPriceDecimals) * _asset_decimals_precision(asset) / tokenPrice / _stablecoin_decimals_precision(stable); } + + function _safe_issuer_fetch(address asset) internal view returns (address) { + (bool success, bytes memory result) = asset.staticcall( + abi.encodeWithSignature("commonState()") + ); + if (success) { + Structs.AssetCommonState memory assetCommonState = abi.decode(result, (Structs.AssetCommonState)); + return assetCommonState.issuer; + } else { return address(0); } + } } diff --git a/contracts/managers/IACfManager.sol b/contracts/managers/IACfManager.sol index 472719d..91f2d87 100644 --- a/contracts/managers/IACfManager.sol +++ b/contracts/managers/IACfManager.sol @@ -5,5 +5,4 @@ import "../shared/ICampaignCommon.sol"; interface IACfManager is ICampaignCommon { function getInfoHistory() external view returns (Structs.InfoEntry[] memory); - function changeOwnership(address newOwner) external; } diff --git a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol index 70ae9dc..25ebf37 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol @@ -48,59 +48,90 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { //------------------------ // CONSTRUCTOR //------------------------ - constructor( - string memory contractFlavor, - string memory contractVersion, - address owner, - address asset, - uint256 tokenPrice, - uint256 softCap, - uint256 minInvestment, - uint256 maxInvestment, - bool whitelistRequired, - string memory info, - address feeManager - ) { - require(owner != address(0), "CfManagerSoftcapVesting: Invalid owner address"); - require(asset != address(0), "CfManagerSoftcapVesting: Invalid asset address"); - require(tokenPrice > 0, "CfManagerSoftcapVesting: Initial price per token must be greater than 0."); - require(maxInvestment >= minInvestment, "CfManagerSoftcapVesting: Max has to be bigger than min investment."); - require(maxInvestment > 0, "CfManagerSoftcapVesting: Max investment has to be bigger than 0."); - IIssuerCommon issuer = IIssuerCommon(IAssetCommon(asset).commonState().issuer); + constructor(Structs.CampaignConstructor memory params) { + require(params.owner != address(0), "CfManagerSoftcapVesting: Invalid owner address"); + require(params.asset != address(0), "CfManagerSoftcapVesting: Invalid asset address"); + require(params.tokenPrice > 0, "CfManagerSoftcapVesting: Initial price per token must be greater than 0."); + require( + params.maxInvestment >= params.minInvestment, + "CfManagerSoftcapVesting: Max has to be bigger than min investment." + ); + require( + params.maxInvestment > 0, + "CfManagerSoftcapVesting: Max investment has to be bigger than 0." + ); + + address fetchedIssuer = _safe_issuer_fetch(params.asset); + address issuerProcessed = fetchedIssuer != address(0) ? fetchedIssuer : params.issuer; + require(issuerProcessed == params.issuer, "CfManagerSoftcapVesting: Invalid issuer provided."); + if (params.whitelistRequired) { + require( + issuerProcessed != address(0), + "CfManagerSoftcapVesting: Issuer must be provided if wallet whitelisting is turned on." + ); + } + + address paymentTokenProcessed = params.paymentToken == address(0) ? + IIssuerCommon(issuerProcessed).commonState().stablecoin : + params.paymentToken; uint256 softCapNormalized = _token_value( - _token_amount_for_investment(softCap, tokenPrice, asset), - tokenPrice, - asset + _token_amount_for_investment( + params.softCap, + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed + ), + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed ); uint256 minInvestmentNormalized = _token_value( - _token_amount_for_investment(minInvestment, tokenPrice, asset), - tokenPrice, - asset + _token_amount_for_investment( + params.minInvestment, + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed + ), + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed ); state = Structs.CfManagerState( - contractFlavor, - contractVersion, + params.contractFlavor, + params.contractVersion, address(this), - owner, - asset, - address(issuer), - issuer.commonState().stablecoin, - tokenPrice, + params.owner, + params.asset, + issuerProcessed, + paymentTokenProcessed, + params.tokenPrice, + params.tokenPriceDecimals, softCapNormalized, minInvestmentNormalized, - maxInvestment, - whitelistRequired, + params.maxInvestment, + params.whitelistRequired, false, false, 0, 0, 0, 0, 0, - info, - feeManager + params.info, + params.feeManager ); vestingState = VestingState(false, 0, 0, 0, true, false); require( - _token_value(IToken(asset).totalSupply(), tokenPrice, asset) >= softCapNormalized, + _token_value( + IToken(params.asset).totalSupply(), + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed + ) >= softCapNormalized, "CfManagerSoftcapVesting: Invalid soft cap." ); + _transferOwnership(params.owner); } //------------------------ @@ -119,7 +150,13 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { //------------------------ function claim(address investor) external finalized vestingStarted { uint256 unreleased = _releasableAmount(investor); - uint256 unreleasedValue = _token_value(unreleased, state.tokenPrice, state.asset); + uint256 unreleasedValue = _token_value( + unreleased, + state.tokenPrice, + state.tokenPriceDecimals, + state.asset, + state.stablecoin + ); require(unreleased > 0, "CfManagerSoftcapVesting: No tokens to be released."); state.totalClaimableTokens -= unreleased; @@ -133,7 +170,7 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { uint256 start, uint256 cliffDuration, uint256 duration - ) external ownerOnly finalized { + ) external onlyOwner finalized { require(!vestingState.vestingStarted, "CfManagerSoftcapVesting: Vesting already started."); require(cliffDuration <= duration, "CfManagerSoftcapVesting: cliffDuration <= duration"); require(duration > 0, "CfManagerSoftcapVesting: duration > 0"); @@ -152,7 +189,7 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { ); } - function revoke() public ownerOnly finalized vestingStarted { + function revoke() public onlyOwner finalized vestingStarted { require(vestingState.revocable, "CfManagerSoftcapVesting: Campaign vesting configuration not revocable."); require(!vestingState.revoked, "CfManagerSoftcapVesting: Campaign vesting already revoked."); @@ -175,7 +212,7 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { state.flavor, state.version, state.contractAddress, - state.owner, + owner(), state.asset, state.issuer, state.stablecoin, diff --git a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol index a3ab756..0349487 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol @@ -6,6 +6,7 @@ import "./ICfManagerSoftcapVestingFactory.sol"; import "../../shared/IAssetCommon.sol"; import "../../shared/ICampaignCommon.sol"; import "../../registry/INameRegistry.sol"; +import "../../shared/Structs.sol"; contract CfManagerSoftcapVestingFactory is ICfManagerSoftcapVestingFactory { @@ -28,40 +29,39 @@ contract CfManagerSoftcapVestingFactory is ICfManagerSoftcapVestingFactory { if (_oldFactory != address(0)) { _addInstances(ICfManagerSoftcapVestingFactory(_oldFactory).getInstances()); } } - function create( - address owner, - string memory mappedName, - address assetAddress, - uint256 initialPricePerToken, - uint256 softCap, - uint256 minInvestment, - uint256 maxInvestment, - bool whitelistRequired, - string memory info, - address nameRegistry, - address feeManager - ) external override returns (address) { - INameRegistry registry = INameRegistry(nameRegistry); + function create(Structs.CampaignFactoryParams memory params) external override returns (address) { + INameRegistry registry = INameRegistry(params.nameRegistry); require( - registry.getCampaign(mappedName) == address(0), + registry.getCampaign(params.mappedName) == address(0), "CfManagerSoftcapVestingFactory: campaign with this name already exists" ); - address cfManagerSoftcap = address(new CfManagerSoftcapVesting( - FLAVOR, - VERSION, - owner, - assetAddress, - initialPricePerToken, - softCap, - minInvestment, - maxInvestment, - whitelistRequired, - info, - feeManager + address cfManagerSoftcap = address( + new CfManagerSoftcapVesting( + Structs.CampaignConstructor( + FLAVOR, + VERSION, + params.owner, + params.assetAddress, + params.issuerAddress, + params.paymentToken, + params.initialPricePerToken, + params.tokenPriceDecimals, + params.softCap, + params.minInvestment, + params.maxInvestment, + params.whitelistRequired, + params.info, + params.feeManager + ) )); _addInstance(cfManagerSoftcap); - registry.mapCampaign(mappedName, cfManagerSoftcap); - emit CfManagerSoftcapVestingCreated(owner, cfManagerSoftcap, address(assetAddress), block.timestamp); + registry.mapCampaign(params.mappedName, cfManagerSoftcap); + emit CfManagerSoftcapVestingCreated( + params.owner, + cfManagerSoftcap, + address(params.assetAddress), + block.timestamp + ); return cfManagerSoftcap; } diff --git a/contracts/managers/crowdfunding-softcap-vesting/ICfManagerSoftcapVestingFactory.sol b/contracts/managers/crowdfunding-softcap-vesting/ICfManagerSoftcapVestingFactory.sol index 7f86c35..0b4e2cf 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/ICfManagerSoftcapVestingFactory.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/ICfManagerSoftcapVestingFactory.sol @@ -2,19 +2,8 @@ pragma solidity ^0.8.0; import "../../shared/ICampaignFactoryCommon.sol"; +import "../../shared/Structs.sol"; interface ICfManagerSoftcapVestingFactory is ICampaignFactoryCommon { - function create( - address owner, - string memory mappedName, - address assetAddress, - uint256 initialPricePerToken, - uint256 softCap, - uint256 minInvestment, - uint256 maxInvestment, - bool whitelistRequired, - string memory info, - address nameRegistry, - address feeManager - ) external returns (address); + function create(Structs.CampaignFactoryParams memory params) external returns (address); } diff --git a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol index 1e91bde..386b950 100644 --- a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol +++ b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol @@ -21,58 +21,86 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { //------------------------ // CONSTRUCTOR //------------------------ - constructor( - string memory contractFlavor, - string memory contractVersion, - address owner, - address asset, - uint256 tokenPrice, - uint256 softCap, - uint256 minInvestment, - uint256 maxInvestment, - bool whitelistRequired, - string memory info, - address feeManager - ) { - require(owner != address(0), "CfManagerSoftcap: Invalid owner address"); - require(asset != address(0), "CfManagerSoftcap: Invalid asset address"); - require(tokenPrice > 0, "CfManagerSoftcap: Initial price per token must be greater than 0."); - require(maxInvestment >= minInvestment, "CfManagerSoftcap: Max has to be bigger than min investment."); - require(maxInvestment > 0, "CfManagerSoftcap: Max investment has to be bigger than 0."); + constructor(Structs.CampaignConstructor memory params) { + require(params.owner != address(0), "CfManagerSoftcap: Invalid owner address"); + require(params.asset != address(0), "CfManagerSoftcap: Invalid asset address"); + require(params.tokenPrice > 0, "CfManagerSoftcap: Initial price per token must be greater than 0."); + require( + params.maxInvestment >= params.minInvestment, + "CfManagerSoftcap: Max has to be bigger than min investment." + ); + require(params.maxInvestment > 0, "CfManagerSoftcap: Max investment has to be bigger than 0."); + address fetchedIssuer = _safe_issuer_fetch(params.asset); + address issuerProcessed = fetchedIssuer != address(0) ? fetchedIssuer : params.issuer; + require(issuerProcessed == params.issuer, "CfManagerSoftcap: Invalid issuer provided."); + if (params.whitelistRequired) { + require( + issuerProcessed != address(0), + "CfManagerSoftcap: Issuer must be provided if wallet whitelisting is turned on." + ); + } + + address paymentTokenProcessed = params.paymentToken == address(0) ? + IIssuerCommon(issuerProcessed).commonState().stablecoin : + params.paymentToken; uint256 softCapNormalized = _token_value( - _token_amount_for_investment(softCap, tokenPrice, asset), - tokenPrice, - asset + _token_amount_for_investment( + params.softCap, + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed + ), + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed ); uint256 minInvestmentNormalized = _token_value( - _token_amount_for_investment(minInvestment, tokenPrice, asset), - tokenPrice, - asset + _token_amount_for_investment( + params.minInvestment, + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed + ), + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed ); - IIssuerCommon issuer = IIssuerCommon(IAssetCommon(asset).commonState().issuer); + state = Structs.CfManagerState( - contractFlavor, - contractVersion, + params.contractFlavor, + params.contractVersion, address(this), - owner, - asset, - address(issuer), - issuer.commonState().stablecoin, - tokenPrice, + params.owner, + params.asset, + issuerProcessed, + paymentTokenProcessed, + params.tokenPrice, + params.tokenPriceDecimals, softCapNormalized, minInvestmentNormalized, - maxInvestment, - whitelistRequired, + params.maxInvestment, + params.whitelistRequired, false, false, 0, 0, 0, 0, 0, - info, - feeManager + params.info, + params.feeManager ); require( - _token_value(IToken(asset).totalSupply(), tokenPrice, asset) >= softCapNormalized, + _token_value( + IToken(params.asset).totalSupply(), + params.tokenPrice, + params.tokenPriceDecimals, + params.asset, + paymentTokenProcessed + ) >= softCapNormalized, "CfManagerSoftcap: Invalid soft cap." ); + _transferOwnership(params.owner); } //------------------------ @@ -100,7 +128,7 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { state.flavor, state.version, state.contractAddress, - state.owner, + owner(), state.asset, state.issuer, state.stablecoin, diff --git a/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol b/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol index d8b62c0..c919ae2 100644 --- a/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol +++ b/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol @@ -27,40 +27,34 @@ contract CfManagerSoftcapFactory is ICfManagerSoftcapFactory { if (_oldFactory != address(0)) { _addInstances(ICfManagerSoftcapFactory(_oldFactory).getInstances()); } } - function create( - address owner, - string memory mappedName, - address assetAddress, - uint256 initialPricePerToken, - uint256 softCap, - uint256 minInvestment, - uint256 maxInvestment, - bool whitelistRequired, - string memory info, - address nameRegistry, - address feeManager - ) external override returns (address) { - INameRegistry registry = INameRegistry(nameRegistry); + function create(Structs.CampaignFactoryParams memory params) external override returns (address) { + INameRegistry registry = INameRegistry(params.nameRegistry); require( - registry.getCampaign(mappedName) == address(0), + registry.getCampaign(params.mappedName) == address(0), "CfManagerSoftcapFactory: campaign with this name already exists" ); - address cfManagerSoftcap = address(new CfManagerSoftcap( - FLAVOR, - VERSION, - owner, - assetAddress, - initialPricePerToken, - softCap, - minInvestment, - maxInvestment, - whitelistRequired, - info, - feeManager + address cfManagerSoftcap = address( + new CfManagerSoftcap( + Structs.CampaignConstructor( + FLAVOR, + VERSION, + params.owner, + params.assetAddress, + params.issuerAddress, + params.paymentToken, + params.initialPricePerToken, + params.tokenPriceDecimals, + params.softCap, + params.minInvestment, + params.maxInvestment, + params.whitelistRequired, + params.info, + params.feeManager + ) )); _addInstance(cfManagerSoftcap); - registry.mapCampaign(mappedName, cfManagerSoftcap); - emit CfManagerSoftcapCreated(owner, cfManagerSoftcap, address(assetAddress), block.timestamp); + registry.mapCampaign(params.mappedName, cfManagerSoftcap); + emit CfManagerSoftcapCreated(params.owner, cfManagerSoftcap, address(params.assetAddress), block.timestamp); return cfManagerSoftcap; } diff --git a/contracts/managers/crowdfunding-softcap/ICfManagerSoftcapFactory.sol b/contracts/managers/crowdfunding-softcap/ICfManagerSoftcapFactory.sol index 9e90f7f..4187571 100644 --- a/contracts/managers/crowdfunding-softcap/ICfManagerSoftcapFactory.sol +++ b/contracts/managers/crowdfunding-softcap/ICfManagerSoftcapFactory.sol @@ -2,19 +2,8 @@ pragma solidity ^0.8.0; import "../../shared/ICampaignFactoryCommon.sol"; +import "../../shared/Structs.sol"; interface ICfManagerSoftcapFactory is ICampaignFactoryCommon { - function create( - address owner, - string memory mappedName, - address assetAddress, - uint256 initialPricePerToken, - uint256 softCap, - uint256 minInvestment, - uint256 maxInvestment, - bool whitelistRequired, - string memory info, - address nameRegistry, - address feeManager - ) external returns (address); + function create(Structs.CampaignFactoryParams memory params) external returns (address); } diff --git a/contracts/managers/nft-basic-mintable/NftBasicMintable.sol b/contracts/managers/nft-basic-mintable/NftBasicMintable.sol new file mode 100644 index 0000000..c81cdfe --- /dev/null +++ b/contracts/managers/nft-basic-mintable/NftBasicMintable.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +contract NftBasicMintable is ERC721, Ownable { + using Counters for Counters.Counter; + Counters.Counter private _tokenIds; + string private baseURI; + + constructor(string memory tokenName, string memory symbol, string memory baseURI_) ERC721(tokenName, symbol) { + baseURI = baseURI_; + } + + function _baseURI() internal override view returns (string memory) { + return baseURI; + } + + function mint(address owner, uint256 count) public onlyOwner { + for (uint256 i = 0; i < count; i++) { + uint256 id = _tokenIds.current(); + _safeMint(owner, id); + _tokenIds.increment(); + } + } +} diff --git a/contracts/managers/nft-rewarder/NftRewarder.sol b/contracts/managers/nft-rewarder/NftRewarder.sol new file mode 100644 index 0000000..aad19df --- /dev/null +++ b/contracts/managers/nft-rewarder/NftRewarder.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +contract NftRewarder is Ownable { + + event AddReward(bytes32 secretHash); + event ClaimReward(address wallet, bytes32 secretHash); + + struct Reward { + bytes32 secretHash; + address token; + uint256 tokenId; + uint256 expiresAt; + } + + mapping (bytes32 => Reward) rewards; + mapping (bytes32 => bool) claimed; + + constructor(address owner) { + _transferOwnership(owner); + } + + function addRewards(Reward[] memory _rewards) public onlyOwner { + for (uint256 i = 0; i < _rewards.length; i++) { + rewards[_rewards[i].secretHash] = _rewards[i]; + emit AddReward(_rewards[i].secretHash); + } + } + + function claimReward(string memory key) public { + bytes memory data = abi.encodePacked(address(this), key); + bytes32 calculatedHash = keccak256(data); + Reward memory reward = rewards[calculatedHash]; + require( + reward.secretHash == calculatedHash, + "Key does not exist!" + ); + require( + !claimed[calculatedHash], + "Reward with this key already claimed!" + ); + require( + block.timestamp <= reward.expiresAt, + "Reward expired!" + ); + claimed[calculatedHash] = true; + IERC721(reward.token).safeTransferFrom( + IERC721(reward.token).ownerOf(reward.tokenId), + msg.sender, + reward.tokenId + ); + emit ClaimReward(msg.sender, calculatedHash); + } +} diff --git a/contracts/managers/rewarder/Rewarder.sol b/contracts/managers/rewarder/Rewarder.sol new file mode 100644 index 0000000..f8cbeb6 --- /dev/null +++ b/contracts/managers/rewarder/Rewarder.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Rewarder is Ownable { + + event AddReward(bytes32 secretHash); + event ClaimReward(address wallet, bytes32 secretHash); + event DrainToken(address token, uint256 amount); + event Drain(uint256 amount); + + struct Reward { + bytes32 secretHash; + address token; + uint256 amount; + uint256 nativeAmount; + uint256 expiresAt; + } + + mapping (bytes32 => Reward) rewards; + mapping (bytes32 => bool) claimed; + + constructor(address owner) { + _transferOwnership(owner); + } + + function addRewards(Reward[] memory _rewards) public onlyOwner { + for (uint256 i = 0; i < _rewards.length; i++) { + rewards[_rewards[i].secretHash] = _rewards[i]; + emit AddReward(_rewards[i].secretHash); + } + } + + function claimReward(string memory key) public { + bytes memory data = abi.encodePacked(address(this), key); + bytes32 calculatedHash = keccak256(data); + Reward memory reward = rewards[calculatedHash]; + require( + reward.secretHash == calculatedHash, + "Key does not exist!" + ); + require( + !claimed[calculatedHash], + "Reward with this key already claimed!" + ); + require( + block.timestamp <= reward.expiresAt, + "Reward expired!" + ); + claimed[calculatedHash] = true; + if (reward.amount > 0) { + IERC20(reward.token).transfer(msg.sender, reward.amount); + } + if (reward.nativeAmount > 0) { + payable(msg.sender).transfer(reward.nativeAmount); + } + emit ClaimReward(msg.sender, calculatedHash); + } + + function drain(address tokenAddress) public onlyOwner { + IERC20 token = IERC20(tokenAddress); + uint256 amount = token.balanceOf(address(this)); + if (amount > 0) { + token.transfer(msg.sender, amount); + emit DrainToken(tokenAddress, amount); + } + } + + function drain() public onlyOwner { + uint256 amount = address(this).balance; + if (amount > 0) { + payable(msg.sender).transfer(amount); + emit Drain(amount); + } + } + + receive() external payable { } + +} diff --git a/contracts/managers/salary-manager/Payroll.sol b/contracts/managers/salary-manager/Payroll.sol new file mode 100644 index 0000000..4514b10 --- /dev/null +++ b/contracts/managers/salary-manager/Payroll.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract Payroll is Ownable { + + event AddPayroll(address payee); + event RemovePayroll(address payee); + event Claim(address payee); + event DrainToken(address token, uint256 amount); + + struct PayrollEntry { + address receiver; + address token; + uint256 amount; + uint256 periodBasis; + uint256 lastReceivedTimestamp; + } + + mapping(address => PayrollEntry) payrolls; + + constructor(address owner) { + _transferOwnership(owner); + } + + function addPayrolls(PayrollEntry[] memory _payrolls) public onlyOwner { + for (uint256 i = 0; i < _payrolls.length; i++) { + address receiver = _payrolls[i].receiver; + payrolls[receiver] = _payrolls[i]; + emit AddPayroll(receiver); + } + } + + function removePayrolls(address[] memory payees) public onlyOwner { + for (uint256 i = 0; i < payees.length; i++) { + address receiver = payees[i]; + delete payrolls[receiver]; + emit RemovePayroll(receiver); + } + } + + function claim(address[] memory payees) public { + for (uint256 i = 0; i < payees.length; i++) { + _claimForPayee(payees[i]); + } + } + + function drain(address tokenAddress) public onlyOwner { + IERC20 token = IERC20(tokenAddress); + uint256 amount = token.balanceOf(address(this)); + if (amount > 0) { + token.transfer(msg.sender, amount); + emit DrainToken(tokenAddress, amount); + } + } + + function _claimForPayee(address payee) internal { + PayrollEntry memory payrollEntry = payrolls[payee]; + require( + payrollEntry.receiver == payee, + "Payroll: does not exist!" + ); + require( + block.timestamp > (payrollEntry.lastReceivedTimestamp + payrollEntry.periodBasis), + "Payroll: next payment not yet unlockd!" + ); + payrollEntry.lastReceivedTimestamp += payrollEntry.periodBasis; + IERC20(payrollEntry.token).transfer(payee, payrollEntry.amount); + emit Claim(payee); + } + +} diff --git a/contracts/managers/supply-chain-manager/SupplyChainManager.sol b/contracts/managers/supply-chain-manager/SupplyChainManager.sol new file mode 100644 index 0000000..f04c7e3 --- /dev/null +++ b/contracts/managers/supply-chain-manager/SupplyChainManager.sol @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + + +// File @openzeppelin/contracts/token/ERC20/IERC20.sol@v4.4.2 + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +contract SupplyChainManager { + + /* + TYPES + */ + + enum State { + EMPTY, + PRODUCED, + PACKED, + SHIPPED, + RECEIVED + } + + struct User { + address wallet; + string info; + State allowedToAdvance; + uint256 addedAt; + bool active; + } + + struct StateChange { + address changedBy; + uint256 changedAt; + State oldState; + State newState; + string note; + } + + struct Product { + uint256 id; + string barcode; + uint256 price; + string description; + State state; + } + + /* + STATE + */ + + address manager; + address manufacturer; + address paymentCurrency; + User[] users; + Product[] products; + mapping(address => uint256) userIdMapping; + mapping(string => bool) productExists; + mapping(address => bool) userExists; + mapping(string => StateChange[]) productHistory; + mapping(address => StateChange[]) userHistory; + + /* + CONSTRUCTOR + */ + + constructor( + address _manager, + address _manufacturer, + address _paymentCurrency + ) { + manager = _manager; + manufacturer = _manufacturer; + paymentCurrency = _paymentCurrency; + } + + /* + MODIFIERS + */ + + modifier onlyManager() { + require( + msg.sender == manager, + "Not manager!" + ); + _; + } + + modifier allowedToAdvance(State state) { + require( + userExists[msg.sender], + "User not registered!" + ); + require( + users[userIdMapping[msg.sender]].active, + "User deactivated!" + ); + require( + users[userIdMapping[msg.sender]].allowedToAdvance == state, + "User missing role!" + ); + _; + } + + /* + SUPPLY CHAIN MANAGEMENT + */ + + function setProduced( + string memory barcode, + uint256 price, + string memory description, + string memory note + ) external allowedToAdvance(State.PRODUCED) { + require( + bytes(barcode).length > 0, + "Barcode is empty!" + ); + require( + !productExists[barcode], + "Barcode already exists!" + ); + productExists[barcode] = true; + Product memory product = Product( + products.length, + barcode, + price, + description, + State.PRODUCED + ); + StateChange memory stateChange = StateChange( + msg.sender, + block.timestamp, + State.EMPTY, + State.PRODUCED, + note + ); + productHistory[barcode].push(stateChange); + userHistory[msg.sender].push(stateChange); + products.push(product); + } + + function setPacked( + uint256 id, + string memory note + ) external allowedToAdvance(State.PACKED) { + require( + products[id].state == State.PRODUCED, + "Invalid product state!" + ); + StateChange memory stateChange = StateChange( + msg.sender, + block.timestamp, + State.PRODUCED, + State.PACKED, + note + ); + products[id].state = State.PACKED; + productHistory[products[id].barcode].push(stateChange); + userHistory[msg.sender].push(stateChange); + } + + function setShipped( + uint256 id, + string memory note + ) external allowedToAdvance(State.SHIPPED) { + require( + products[id].state == State.PACKED, + "Invalid product state!" + ); + StateChange memory stateChange = StateChange( + msg.sender, + block.timestamp, + State.PACKED, + State.SHIPPED, + note + ); + products[id].state = State.SHIPPED; + productHistory[products[id].barcode].push(stateChange); + userHistory[msg.sender].push(stateChange); + } + + function setReceived( + uint256 id, + string memory note + ) external allowedToAdvance(State.RECEIVED) { + require( + products[id].state == State.SHIPPED, + "Invalid product state!" + ); + StateChange memory stateChange = StateChange( + msg.sender, + block.timestamp, + State.SHIPPED, + State.RECEIVED, + note + ); + products[id].state = State.RECEIVED; + productHistory[products[id].barcode].push(stateChange); + userHistory[msg.sender].push(stateChange); + IERC20(paymentCurrency).transfer(manufacturer, products[id].price); + } + + /* + USERS MANAGEMENT + */ + + function addUser( + address wallet, + string memory info, + State role + ) external onlyManager { + require( + !userExists[wallet], + "User already exists!" + ); + userExists[wallet] = true; + userIdMapping[wallet] = users.length; + User memory user = User( + wallet, + info, + role, + block.timestamp, + true + ); + users.push(user); + } + + function updateUserStatus( + address wallet, + bool active + ) external onlyManager { + require( + userExists[wallet], + "User does not exist" + ); + users[userIdMapping[wallet]].active = active; + } + + /* + READ FUNCTIONS + */ + + function getUsers() external view returns (User[] memory) { + return users; + } + + function getProducts() external view returns (Product[] memory) { + return products; + } + + function getUserHistory(address user) external view returns (StateChange[] memory) { + require( + userExists[user], + "User does not exist!" + ); + return userHistory[user]; + } + + function getProductHistory(string memory barcode) external view returns (StateChange[] memory) { + require( + productExists[barcode], + "Product does not exist!" + ); + return productHistory[barcode]; + } + +} diff --git a/contracts/services/DeployerService.sol b/contracts/services/DeployerService.sol index 331ffc6..d3c79db 100644 --- a/contracts/services/DeployerService.sol +++ b/contracts/services/DeployerService.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; import "../asset/IAsset.sol"; import "../asset/IAssetFactory.sol"; import "../asset-transferable/IAssetTransferable.sol"; @@ -65,7 +66,9 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -91,7 +94,9 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -122,7 +127,9 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -148,7 +155,9 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -172,7 +181,9 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -226,18 +237,24 @@ contract DeployerService is IDeployerService { request.assetInfo ) )); - ICfManagerSoftcap campaign = ICfManagerSoftcap(request.cfManagerSoftcapFactory.create( - address(this), - request.cfManagerMappedName, - address(asset), - request.cfManagerPricePerToken, - request.cfManagerSoftcap, - request.cfManagerSoftcapMinInvestment, - request.cfManagerSoftcapMaxInvestment, - request.cfManagerWhitelistRequired, - request.cfManagerInfo, - request.nameRegistry, - request.feeManager + ICfManagerSoftcap campaign = ICfManagerSoftcap( + request.cfManagerSoftcapFactory.create( + Structs.CampaignFactoryParams( + address(this), + request.cfManagerMappedName, + address(asset), + address(issuer), + request.cfManagerPaymentToken, + request.cfManagerPricePerToken, + request.cfManagerTokenPriceDecimals, + request.cfManagerSoftcap, + request.cfManagerSoftcapMinInvestment, + request.cfManagerSoftcapMaxInvestment, + request.cfManagerWhitelistRequired, + request.cfManagerInfo, + request.nameRegistry, + request.feeManager + ) )); // Whitelist owners @@ -254,9 +271,9 @@ contract DeployerService is IDeployerService { // Transfer ownerships from address(this) to the actual owner wallets issuer.changeWalletApprover(request.issuerWalletApprover); - issuer.changeOwnership(request.issuerOwner); + Ownable(address(issuer)).transferOwnership(request.issuerOwner); asset.changeOwnership(request.assetOwner); - campaign.changeOwnership(request.cfManagerOwner); + Ownable(address(campaign)).transferOwnership(request.cfManagerOwner); emit DeployIssuerAssetCampaign(msg.sender, address(issuer), address(asset), address(campaign), block.timestamp); } @@ -279,18 +296,24 @@ contract DeployerService is IDeployerService { request.assetInfo ) )); - ICfManagerSoftcap campaign = ICfManagerSoftcap(request.cfManagerSoftcapFactory.create( - address(this), - request.cfManagerMappedName, - address(asset), - request.cfManagerPricePerToken, - request.cfManagerSoftcap, - request.cfManagerSoftcapMinInvestment, - request.cfManagerSoftcapMaxInvestment, - request.cfManagerWhitelistRequired, - request.cfManagerInfo, - request.nameRegistry, - request.feeManager + ICfManagerSoftcap campaign = ICfManagerSoftcap( + request.cfManagerSoftcapFactory.create( + Structs.CampaignFactoryParams( + address(this), + request.cfManagerMappedName, + address(asset), + request.issuer, + request.cfManagerPaymentToken, + request.cfManagerPricePerToken, + request.cfManagerTokenPriceDecimals, + request.cfManagerSoftcap, + request.cfManagerSoftcapMinInvestment, + request.cfManagerSoftcapMaxInvestment, + request.cfManagerWhitelistRequired, + request.cfManagerInfo, + request.nameRegistry, + request.feeManager + ) )); // Transfer tokens to sell to the campaign, transfer the rest to the asset owner's wallet @@ -303,7 +326,7 @@ contract DeployerService is IDeployerService { // Transfer ownerships from address(this) to the actual owner wallets asset.freezeTransfer(); asset.changeOwnership(request.assetOwner); - campaign.changeOwnership(request.cfManagerOwner); + Ownable(address(campaign)).transferOwnership(request.cfManagerOwner); emit DeployAssetCampaign(msg.sender, address(asset), address(campaign), block.timestamp); } @@ -338,18 +361,25 @@ contract DeployerService is IDeployerService { ) ); - ICfManagerSoftcap campaign = ICfManagerSoftcap(request.cfManagerSoftcapFactory.create( - address(this), - request.cfManagerMappedName, - address(asset), - request.cfManagerPricePerToken, - request.cfManagerSoftcap, - request.cfManagerSoftcapMinInvestment, - request.cfManagerSoftcapMaxInvestment, - request.cfManagerWhitelistRequired, - request.cfManagerInfo, - request.nameRegistry, - request.feeManager + ICfManagerSoftcap campaign = ICfManagerSoftcap( + request.cfManagerSoftcapFactory.create( + Structs.CampaignFactoryParams( + address(this), + request.cfManagerMappedName, + address(asset), + address(issuer), + request.cfManagerPaymentToken, + request.cfManagerPricePerToken, + request.cfManagerTokenPriceDecimals, + request.cfManagerSoftcap, + request.cfManagerSoftcapMinInvestment, + request.cfManagerSoftcapMaxInvestment, + request.cfManagerWhitelistRequired, + request.cfManagerInfo, + request.nameRegistry, + request.feeManager + ) + )); // Whitelist issuer owner @@ -364,9 +394,9 @@ contract DeployerService is IDeployerService { // Transfer ownerships from address(this) to the actual owner wallets issuer.changeWalletApprover(request.issuerWalletApprover); - issuer.changeOwnership(request.issuerOwner); + Ownable(address(issuer)).transferOwnership(request.cfManagerOwner); asset.changeOwnership(request.assetOwner); - campaign.changeOwnership(request.cfManagerOwner); + Ownable(address(campaign)).transferOwnership(request.cfManagerOwner); emit DeployIssuerAssetTransferableCampaign( msg.sender, @@ -395,18 +425,24 @@ contract DeployerService is IDeployerService { request.assetInfo ) )); - ICfManagerSoftcap campaign = ICfManagerSoftcap(request.cfManagerSoftcapFactory.create( - address(this), - request.cfManagerMappedName, - address(asset), - request.cfManagerPricePerToken, - request.cfManagerSoftcap, - request.cfManagerSoftcapMinInvestment, - request.cfManagerSoftcapMaxInvestment, - request.cfManagerWhitelistRequired, - request.cfManagerInfo, - request.nameRegistry, - request.feeManager + ICfManagerSoftcap campaign = ICfManagerSoftcap( + request.cfManagerSoftcapFactory.create( + Structs.CampaignFactoryParams( + address(this), + request.cfManagerMappedName, + address(asset), + request.issuer, + request.cfManagerPaymentToken, + request.cfManagerPricePerToken, + request.cfManagerTokenPriceDecimals, + request.cfManagerSoftcap, + request.cfManagerSoftcapMinInvestment, + request.cfManagerSoftcapMaxInvestment, + request.cfManagerWhitelistRequired, + request.cfManagerInfo, + request.nameRegistry, + request.feeManager + ) )); // Transfer tokens to sell to the campaign, transfer the rest to the asset owner's wallet @@ -418,7 +454,7 @@ contract DeployerService is IDeployerService { // Transfer ownerships from address(this) to the actual owner wallets asset.changeOwnership(request.assetOwner); - campaign.changeOwnership(request.cfManagerOwner); + Ownable(address(campaign)).transferOwnership(request.cfManagerOwner); emit DeployAssetCampaign(msg.sender, address(asset), address(campaign), block.timestamp); } @@ -439,18 +475,24 @@ contract DeployerService is IDeployerService { ) ) ); - ICfManagerSoftcapVesting campaign = ICfManagerSoftcapVesting(request.cfManagerSoftcapVestingFactory.create( - address(this), - request.cfManagerMappedName, - address(asset), - request.cfManagerPricePerToken, - request.cfManagerSoftcap, - request.cfManagerSoftcapMinInvestment, - request.cfManagerSoftcapMaxInvestment, - request.cfManagerWhitelistRequired, - request.cfManagerInfo, - request.nameRegistry, - request.feeManager + ICfManagerSoftcapVesting campaign = ICfManagerSoftcapVesting( + request.cfManagerSoftcapVestingFactory.create( + Structs.CampaignFactoryParams( + address(this), + request.cfManagerMappedName, + address(asset), + request.issuer, + request.cfManagerPaymentToken, + request.cfManagerPricePerToken, + request.cfManagerTokenPriceDecimals, + request.cfManagerSoftcap, + request.cfManagerSoftcapMinInvestment, + request.cfManagerSoftcapMaxInvestment, + request.cfManagerWhitelistRequired, + request.cfManagerInfo, + request.nameRegistry, + request.feeManager + ) )); // Transfer tokens to sell to the campaign, transfer the rest to the asset owner's wallet @@ -462,7 +504,7 @@ contract DeployerService is IDeployerService { // Transfer ownerships from address(this) to the actual owner wallets asset.changeOwnership(request.assetOwner); - campaign.changeOwnership(request.cfManagerOwner); + Ownable(address(campaign)).transferOwnership(request.cfManagerOwner); emit DeployAssetCampaign(msg.sender, address(asset), address(campaign), block.timestamp); } diff --git a/contracts/services/QueryService.sol b/contracts/services/QueryService.sol index 53db7c0..94a667b 100644 --- a/contracts/services/QueryService.sol +++ b/contracts/services/QueryService.sol @@ -517,12 +517,13 @@ contract QueryService is IQueryService { uint256 tokenAmount, IAssetCommon token, IToken stablecoin, - uint256 price + uint256 price, + uint8 priceDecimals ) external view returns (uint256) { return tokenAmount * price * (10 ** stablecoin.decimals()) - / token.priceDecimalsPrecision() + / (10 ** priceDecimals) / (10 ** IToken(address(token)).decimals()); } diff --git a/contracts/shared/IAssetCommon.sol b/contracts/shared/IAssetCommon.sol index 4839d3b..9170f19 100644 --- a/contracts/shared/IAssetCommon.sol +++ b/contracts/shared/IAssetCommon.sol @@ -12,6 +12,5 @@ interface IAssetCommon is IVersioned { // READ function commonState() external view returns (Structs.AssetCommonState memory); - function priceDecimalsPrecision() external view returns (uint256); } diff --git a/contracts/shared/Structs.sol b/contracts/shared/Structs.sol index 5eba6f2..52c9b2c 100644 --- a/contracts/shared/Structs.sol +++ b/contracts/shared/Structs.sol @@ -44,6 +44,40 @@ contract Structs { AssetCommonState asset; string mappedName; } + + struct CampaignFactoryParams { + address owner; + string mappedName; + address assetAddress; + address issuerAddress; + address paymentToken; + uint256 initialPricePerToken; + uint8 tokenPriceDecimals; + uint256 softCap; + uint256 minInvestment; + uint256 maxInvestment; + bool whitelistRequired; + string info; + address nameRegistry; + address feeManager; + } + + struct CampaignConstructor { + string contractFlavor; + string contractVersion; + address owner; + address asset; + address issuer; + address paymentToken; + uint256 tokenPrice; + uint8 tokenPriceDecimals; + uint256 softCap; + uint256 minInvestment; + uint256 maxInvestment; + bool whitelistRequired; + string info; + address feeManager; + } struct CampaignCommonState { string flavor; @@ -57,6 +91,7 @@ contract Structs { bool finalized; bool canceled; uint256 pricePerToken; + uint8 tokenPriceDecimals; uint256 fundsRaised; uint256 tokensSold; } @@ -87,6 +122,7 @@ contract Structs { bool state; uint256 stateUpdatedAt; uint256 price; + uint8 priceDecimals; uint256 priceUpdatedAt; uint256 priceValidUntil; uint256 capturedSupply; @@ -215,6 +251,7 @@ contract Structs { uint256 totalAmountRaised; uint256 totalTokensSold; uint256 highestTokenSellPrice; + uint8 highestTokenSellPriceDecimals; uint256 totalTokensLocked; uint256 totalTokensLockedAndLiquidated; bool liquidated; @@ -240,6 +277,7 @@ contract Structs { uint256 totalAmountRaised; uint256 totalTokensSold; uint256 highestTokenSellPrice; + uint8 highestTokenSellPriceDecimals; bool liquidated; uint256 liquidationFundsTotal; uint256 liquidationTimestamp; @@ -274,6 +312,7 @@ contract Structs { address issuer; address stablecoin; uint256 tokenPrice; + uint8 tokenPriceDecimals; uint256 softCap; uint256 minInvestment; uint256 maxInvestment; diff --git a/package-lock.json b/package-lock.json index 89ea359..368092f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "tokenizer-prototype", - "version": "1.0.32", + "version": "1.0.33", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -8603,7 +8603,8 @@ }, "keccak": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", "dev": true, "requires": { "node-addon-api": "^2.0.0", @@ -9150,7 +9151,8 @@ }, "node-addon-api": { "version": "2.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", "dev": true }, "node-fetch": { @@ -9161,7 +9163,8 @@ }, "node-gyp-build": { "version": "4.2.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", "dev": true }, "normalize-url": { diff --git a/test/TestData.ts b/test/TestData.ts index 80e804d..9692ca2 100644 --- a/test/TestData.ts +++ b/test/TestData.ts @@ -71,6 +71,7 @@ export class TestData { assetWhitelistRequiredForLiquidationClaim = true; assetTokenSupply = 300000; // 300k tokens total supply campaignInitialPricePerToken = 10000; // 1$ per token + campaignTokenPriceDecimals = 4; // 4 decimals token price precision maxTokensToBeSold = 200000; // 200k tokens to be sold at most (200k $$$ to be raised at most) campaignSoftCap = 100000; // minimum $100k funds raised has to be reached for campaign to succeed campaignMinInvestment = 10000; // $10k min investment per user @@ -175,7 +176,9 @@ export class TestData { this.assetInfoHash, issuerOwnerAddress, this.campaignAnsName, + this.stablecoin.address, this.campaignInitialPricePerToken, + this.campaignTokenPriceDecimals, this.campaignSoftCap, this.campaignMinInvestment, this.campaignMaxInvestment, @@ -211,7 +214,9 @@ export class TestData { this.assetInfoHash, issuerOwnerAddress, this.campaignAnsName, + this.stablecoin.address, this.campaignInitialPricePerToken, + this.campaignTokenPriceDecimals, this.campaignSoftCap, this.campaignMinInvestment, this.campaignMaxInvestment, @@ -246,7 +251,9 @@ export class TestData { this.assetInfoHash, issuerOwnerAddress, this.campaignAnsName, + this.stablecoin.address, this.campaignInitialPricePerToken, + this.campaignTokenPriceDecimals, this.campaignSoftCap, this.campaignMinInvestment, this.campaignMaxInvestment, @@ -287,7 +294,7 @@ export class TestData { .approve(this.asset.address, await helpers.parseStablecoin(liquidationFunds, this.stablecoin)); await helpers .registerAsset(this.assetManager, this.apxRegistry, this.asset.address, this.asset.address); - await helpers.updatePrice(this.priceManager, this.apxRegistry, this.asset, 1, 60); + await helpers.updatePrice(this.priceManager, this.apxRegistry, this.asset, 1, 4, 60); await helpers.liquidate(this.issuerOwner, this.asset, this.stablecoin, liquidationFunds); } } diff --git a/test/asset-transferable/asset-transferable-full-flow.ts b/test/asset-transferable/asset-transferable-full-flow.ts index 9b627e1..ddac317 100644 --- a/test/asset-transferable/asset-transferable-full-flow.ts +++ b/test/asset-transferable/asset-transferable-full-flow.ts @@ -121,7 +121,7 @@ describe("Asset transferable - full test", function () { // update market price for asset // price: $0.70, expiry: 60 seconds - await helpers.updatePrice(testData.priceManager, testData.apxRegistry, testData.asset, 11000, 60); + await helpers.updatePrice(testData.priceManager, testData.apxRegistry, testData.asset, 11000, 4, 60); //// Asset owner liquidates asset // Asset was crowdfunded at $1/token and is now trading at $1.10/token so the total supply must be liquidated diff --git a/test/asset/asset-full-flow.ts b/test/asset/asset-full-flow.ts index 4b71538..b4c3c6e 100644 --- a/test/asset/asset-full-flow.ts +++ b/test/asset/asset-full-flow.ts @@ -160,7 +160,7 @@ describe("Asset - full test", function () { ////// update market price for asset ////// price: $0.70, expiry: 60 seconds - await helpers.updatePrice(testData.priceManager, testData.apxRegistry, mirroredAsset, 11000, 60); + await helpers.updatePrice(testData.priceManager, testData.apxRegistry, mirroredAsset, 11000, 4, 60); //// Asset owner liquidates asset // Asset was crowdfunded at $1/token and is now trading at $1.10/token so the total supply must be liquidated diff --git a/test/campaign/campaign-deployer.ts b/test/campaign/campaign-deployer.ts index c13bcb7..6be468c 100644 --- a/test/campaign/campaign-deployer.ts +++ b/test/campaign/campaign-deployer.ts @@ -7,6 +7,7 @@ export async function createCampaign( owner: String, mappedName: String, asset: Contract, + issuer: Contract, pricePerToken: Number, softCapWei: Number, minInvestmentWei: Number, @@ -18,11 +19,14 @@ export async function createCampaign( feeRegistry: Contract, opts?: { logOutput: boolean } ): Promise { - const cfManagerTx = await cfManagerFactory.create( + const cfManagerTx = await cfManagerFactory.create([ owner, mappedName, asset.address, + issuer.address, + ethers.constants.AddressZero, pricePerToken, + 0, softCapWei, minInvestmentWei, maxInvestmentWei, @@ -30,7 +34,7 @@ export async function createCampaign( info, nameRegistry.address, feeRegistry.address - ); + ]); const receipt = await ethers.provider.waitForTransaction(cfManagerTx.hash); for (const recLog of receipt.logs) { try { diff --git a/test/campaign/campaign-tests.ts b/test/campaign/campaign-tests.ts index 8451343..6b09352 100644 --- a/test/campaign/campaign-tests.ts +++ b/test/campaign/campaign-tests.ts @@ -43,6 +43,7 @@ describe("Covers important tests for all campaign flavors", function () { issuerOwnerAddress, "regular-campaign-successful-1", asset, + testData.issuer, 10000, // $1 price per token 10000000, // $10 softCap (taken from real example) 10000000, // $10 min per user investment @@ -178,6 +179,7 @@ describe("Covers important tests for all campaign flavors", function () { issuerOwnerAddress, "regular-campaign-successful-2", asset, + testData.issuer, 111, // $0.111 price per token 2111999997, // ~$2112 softCap (taken from real example) 550000000, // $550 min per user investment @@ -271,6 +273,7 @@ describe("Covers important tests for all campaign flavors", function () { issuerOwnerAddress, "failed-campaign", asset, + testData.issuer, 10000, // $1 price per token 10000000, // $10 softCap (taken from real example) 10000000, // $10 min per user investment @@ -353,6 +356,7 @@ describe("Covers important tests for all campaign flavors", function () { issuerOwnerAddress, "min-per-user-test", asset, + testData.issuer, 30000, // $3 price per token 10000000, // $10 softCap 2000000, // $2 min per user investment @@ -406,6 +410,7 @@ describe("Covers important tests for all campaign flavors", function () { issuerOwnerAddress, "min-per-user-test", asset, + testData.issuer, 30000, // $3 price per token 10000000, // $10 softCap 2000000, // $2 min per user investment diff --git a/test/managers/rewarder-test.ts b/test/managers/rewarder-test.ts new file mode 100644 index 0000000..db55a10 --- /dev/null +++ b/test/managers/rewarder-test.ts @@ -0,0 +1,112 @@ +// @ts-ignore +import { ethers } from "hardhat"; +import { Contract, Signer } from "ethers"; +import * as helpers from "../../util/helpers"; +import { expect } from "chai"; +import { describe, it } from "mocha"; +import { Rewarder, USDC } from "../../typechain"; +import { solidityKeccak256 } from "ethers/lib/utils"; +import { advanceBlockTime } from "../../util/utils"; + +describe("Rewarder test", function () { + + let rewarder: Rewarder + let usdc: USDC + let owner: Signer + let ownerAddress: string + let claimer: Signer + let claimerAddress: string + + beforeEach(async function () { + const accounts: Signer[] = await ethers.getSigners(); + owner = accounts[0]; + claimer = accounts[1]; + ownerAddress = await owner.getAddress(); + claimerAddress = await claimer.getAddress(); + + let rewarderFactory = await ethers.getContractFactory("Rewarder", owner); + rewarder = await rewarderFactory.deploy(ownerAddress); + usdc = (await helpers.deployStablecoin(owner, 1000, 18)) as USDC; + + usdc.connect(owner).transfer(rewarder.address, await helpers.parseStablecoin(10, usdc)); + await owner.sendTransaction({ + to: rewarder.address, + value: ethers.utils.parseUnits("10", "ether") + }); + console.log("native balance", (await ethers.provider.getBalance(rewarder.address)).toString()); + console.log("usdc balance", (await usdc.balanceOf(rewarder.address)).toString()); + }); + + it('is possible to claim reward using the valid key', async () => { + const secretKey = "secret-key"; + const hash = solidityKeccak256(["address", "string"], [rewarder.address, secretKey]); + const usdcRewardAmount = await helpers.parseStablecoin("1", usdc); + const nativeRewardAmount = usdcRewardAmount; + const expiresAt = Date.now() + 100; // expires in 100 seconds + + const forbiddenAddReward = rewarder.connect(claimer).addRewards([ + { + secretHash: hash, + token: usdc.address, + amount: usdcRewardAmount, + nativeAmount: nativeRewardAmount, + expiresAt: expiresAt + } + ]); + await expect(forbiddenAddReward).to.be.revertedWith("Ownable: caller is not the owner"); + + await rewarder.addRewards([ + { + secretHash: hash, + token: usdc.address, + amount: usdcRewardAmount, + nativeAmount: nativeRewardAmount, + expiresAt: expiresAt + } + ]); + + const nonexistingKeyClaim = rewarder.connect(claimer).claimReward("non-existing-key"); + await expect(nonexistingKeyClaim).to.be.revertedWith("Key does not exist!"); + + const claimerNativeBalancePreClaim = await ethers.provider.getBalance(claimerAddress) + await rewarder.connect(claimer).claimReward(secretKey); + expect(await usdc.balanceOf(claimerAddress)).to.be.equal(usdcRewardAmount); + expect((await ethers.provider.getBalance(claimerAddress)).gt(claimerNativeBalancePreClaim)).to.be.true; + + const repeatedClaim = rewarder.connect(claimer).claimReward(secretKey); + await expect(repeatedClaim).to.be.revertedWith("Reward with this key already claimed!"); + + const forbiddenDrainToken = rewarder.connect(claimer)["drain(address)"](usdc.address); + await expect(forbiddenDrainToken).to.be.revertedWith("Ownable: caller is not the owner"); + + const forbiddenDrainNativeToken = rewarder.connect(claimer)["drain()"](); + await expect(forbiddenDrainNativeToken).to.be.revertedWith("Ownable: caller is not the owner"); + + const expiredKey = "expired-key"; + const expiredHash = solidityKeccak256(["address", "string"], [rewarder.address, expiredKey]); + const expiredTimestamp = Date.now() + 10; // expires in 10 seconds + await rewarder.addRewards([ + { + secretHash: expiredHash, + token: usdc.address, + amount: usdcRewardAmount, + nativeAmount: nativeRewardAmount, + expiresAt: expiredTimestamp + } + ]); + await advanceBlockTime(expiredTimestamp + 1); + const expiredRewardClaim = rewarder.connect(claimer).claimReward(expiredKey); + await expect(expiredRewardClaim).to.be.revertedWith("Reward expired!"); + + const ownerUsdcBalanceBeforeDrain = await usdc.balanceOf(ownerAddress); + const ownerNativeBalanceBeforeDrain = await ethers.provider.getBalance(ownerAddress); + await rewarder["drain(address)"](usdc.address); + await rewarder["drain()"](); + + expect(await usdc.balanceOf(rewarder.address)).to.be.equal(0); + expect(await ethers.provider.getBalance(rewarder.address)).to.be.equal(0); + expect((await usdc.balanceOf(ownerAddress)).gt(ownerUsdcBalanceBeforeDrain)).to.be.true; + expect((await ethers.provider.getBalance(ownerAddress)).gt(ownerNativeBalanceBeforeDrain)).to.be.true; + }); + +}); \ No newline at end of file diff --git a/test/managers/supply-chain-manager-test.ts b/test/managers/supply-chain-manager-test.ts new file mode 100644 index 0000000..c46bf7f --- /dev/null +++ b/test/managers/supply-chain-manager-test.ts @@ -0,0 +1,265 @@ +// @ts-ignore +import { ethers } from "hardhat"; +import { Signer, Wallet } from "ethers"; +import * as helpers from "../../util/helpers"; +import { expect } from "chai"; +import { describe, it } from "mocha"; +import { USDC, SupplyChainManager } from "../../typechain"; + +describe("Supply chain manager test", function () { + + enum Role { + PRODUCTION = 1, + PACKING = 2, + SHIPPING = 3, + BUYER = 4 + } + + let supplyChainManager: SupplyChainManager; + let usdc: USDC; + let manager: Signer; + let managerAddress: string; + let manufacturer: Signer; + let manufacturerAdddress: string; + let producer: Signer; + let producerAddress: string; + let packager: Signer; + let packagerAddress: string; + let shipper: Signer; + let shipperAddress: string; + let receiver: Signer; + let receiverAddress: string; + + beforeEach(async function () { + const accounts: Signer[] = await ethers.getSigners(); + manager = accounts[0]; + manufacturer = accounts[1]; + producer = accounts[2]; + packager = accounts[3]; + shipper = accounts[4]; + receiver = accounts[5]; + managerAddress = await manager.getAddress(); + manufacturerAdddress = await manufacturer.getAddress(); + producerAddress = await producer.getAddress(); + packagerAddress = await packager.getAddress(); + shipperAddress = await shipper.getAddress(); + receiverAddress = await receiver.getAddress(); + + let supplyChainManagerFactory = await ethers.getContractFactory("SupplyChainManager", manager); + usdc = (await helpers.deployStablecoin(manager, 1000, 18)) as USDC; + supplyChainManager = await supplyChainManagerFactory.connect(manager).deploy( + managerAddress, + manufacturerAdddress, + usdc.address + ); + }); + + it('should be able to run one full product lifetime flow (production to shipping)', async () => { + + // MANAGER CAN ADD USERS + await supplyChainManager.addUser( + producerAddress, + "main producer", + Role.PRODUCTION + ); + await supplyChainManager.addUser( + packagerAddress, + "main packager", + Role.PACKING + ); + await supplyChainManager.addUser( + shipperAddress, + "dhl", + Role.SHIPPING + ); + await supplyChainManager.addUser( + receiverAddress, + "buying customer", + Role.BUYER + ); + + // SETUP DEACTIVATED USER + const deactivated = Wallet.createRandom().connect(ethers.provider); + const deactivatedAddress = await deactivated.getAddress(); + await supplyChainManager.addUser( + deactivatedAddress, + "deactivated address", + Role.BUYER + ); + await supplyChainManager.updateUserStatus( + deactivatedAddress, + false + ); + await manager.sendTransaction({ + to: deactivatedAddress, + value: ethers.utils.parseEther("1.0") + }); + + // SETUP UNREGISTERED USER + + const unregistered = Wallet.createRandom().connect(ethers.provider); + const unregisteredAddress = await unregistered.getAddress(); + await manager.sendTransaction({ + to: unregisteredAddress, + value: ethers.utils.parseEther("1.0") + }); + + // ALREADY EXISTING USERS CAN NOT BE ADDED + const failedRepeatedAddUserTx = supplyChainManager.addUser( + producerAddress, + "random info", + Role.PRODUCTION + ); + await expect(failedRepeatedAddUserTx).to.be.revertedWith("User already exists!"); + + // OTHERS CAN NOT ADD USERS + const randomWallet = Wallet.createRandom(); + const randomWalletAddress = await randomWallet.getAddress(); + const failedAddUserTx = supplyChainManager.connect(packager).addUser( + randomWalletAddress, + "random info", + Role.PACKING + ); + await expect(failedAddUserTx).to.be.revertedWith("Not manager!"); + + // CAN NOT UPDATE NON-EXISTING USER + const failedUpdateNonExistingUserTx = supplyChainManager.updateUserStatus( + randomWalletAddress, + true + ); + await expect(failedUpdateNonExistingUserTx).to.be.revertedWith("User does not exist"); + + // OTHERS CAN NOT UPDATE USERS + const failedForbiddenUpdateUserTx = supplyChainManager.connect(packager).updateUserStatus( + shipperAddress, + false + ); + await expect(failedForbiddenUpdateUserTx).to.be.revertedWith("Not manager!"); + + // PRODUCE PRODUCT + const barcode = "123abc456def"; + const price = 100; + await supplyChainManager.connect(producer).setProduced( + barcode, + price, + "product description", + "action description" + ); + await expect( + supplyChainManager.connect(producer).setProduced( + barcode, + price, + "product description", + "action description" + ) + ).to.be.revertedWith("Barcode already exists!"); + await expect( + supplyChainManager.connect(producer).setProduced( + "", + price, + "product description", + "action description" + ) + ).to.be.revertedWith("Barcode is empty!"); + await expect( + supplyChainManager.connect(unregistered).setProduced(barcode, price, "", "") + ).to.be.revertedWith("User not registered!"); + await expect( + supplyChainManager.connect(deactivated).setProduced(barcode, price, "", "") + ).to.be.revertedWith("User deactivated!"); + await expect( + supplyChainManager.connect(packager).setProduced(barcode, price, "", "") + ).to.be.revertedWith("User missing role!"); + + // PACK PRODUCT + await supplyChainManager.connect(packager).setPacked(0, "pack action description"); + await expect( + supplyChainManager.connect(unregistered).setPacked(0, "pack action description") + ).to.be.revertedWith("User not registered!"); + await expect( + supplyChainManager.connect(deactivated).setPacked(0, "pack action description") + ).to.be.revertedWith("User deactivated!"); + await expect( + supplyChainManager.connect(producer).setPacked(0, "pack action description") + ).to.be.revertedWith("User missing role!"); + await expect( + supplyChainManager.connect(packager).setPacked(0, "pack action description") + ).to.be.revertedWith("Invalid product state!"); + + // SHIP PRODUCT + await supplyChainManager.connect(shipper).setShipped(0, "ship action description"); + await expect( + supplyChainManager.connect(unregistered).setShipped(0, "ship action description") + ).to.be.revertedWith("User not registered!"); + await expect( + supplyChainManager.connect(deactivated).setShipped(0, "ship action description") + ).to.be.revertedWith("User deactivated!"); + await expect( + supplyChainManager.connect(producer).setShipped(0, "ship action description") + ).to.be.revertedWith("User missing role!"); + await expect( + supplyChainManager.connect(shipper).setShipped(0, "ship action description") + ).to.be.revertedWith("Invalid product state!"); + + // FAILED RECEIVE PRODUCT (receiver did not pay for item) + await expect( + supplyChainManager.connect(receiver).setReceived(0, "receive action description") + ).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + + // RECEIVER PAYS FOR PRODUCT + await usdc.transfer(supplyChainManager.address, price); + await supplyChainManager.connect(receiver).setReceived(0, "receive action description"); + expect(await usdc.balanceOf(manufacturerAdddress)).to.be.equal(price); + expect(await usdc.balanceOf(supplyChainManager.address)).to.be.equal(0); + + await expect( + supplyChainManager.connect(unregistered).setReceived(0, "receive action description") + ).to.be.revertedWith("User not registered!"); + await expect( + supplyChainManager.connect(deactivated).setReceived(0, "receive action description") + ).to.be.revertedWith("User deactivated!"); + await expect( + supplyChainManager.connect(producer).setReceived(0, "receive action description") + ).to.be.revertedWith("User missing role!"); + await expect( + supplyChainManager.connect(receiver).setReceived(0, "receive action description") + ).to.be.revertedWith("Invalid product state!"); + + // TEST QUERY METHODS + console.log( + "getUsers()", + await supplyChainManager.getUsers() + ); + + console.log( + "getProducts()", + await supplyChainManager.getProducts() + ); + + console.log( + "getUserHistory(producer)", + await supplyChainManager.getUserHistory(producerAddress) + ); + + console.log( + "getUserHistory(packager)", + await supplyChainManager.getUserHistory(packagerAddress) + ); + + console.log( + "getUserHistory(shipper)", + await supplyChainManager.getUserHistory(shipperAddress) + ); + + console.log( + "getUserHistory(receiver)", + await supplyChainManager.getUserHistory(receiverAddress) + ); + + console.log( + "getProductHistory(product)", + await supplyChainManager.getProductHistory(barcode) + ); + }); + +}); diff --git a/util/deployer-service.ts b/util/deployer-service.ts index c4765e2..ea96ac8 100644 --- a/util/deployer-service.ts +++ b/util/deployer-service.ts @@ -13,13 +13,16 @@ export async function createIssuerAssetCampaign( assetOwner: String, assetMappedName: String, assetInitialTokenSupply: Number, - assetWhitelistRequired: boolean, + assetWhitelistRequiredForRevenueClaim: boolean, + assetWhitelistRequiredForLiquidationClaim: boolean, assetName: String, assetSymbol: String, assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -32,6 +35,7 @@ export async function createIssuerAssetCampaign( deployerService: Contract, apxRegistry: Contract, nameRegistry: Contract, + campaignFeeManager: Contract, opts?: { logOutput: boolean } ): Promise> { const stablecoin = await ethers.getContractAt("USDC", issuerStablecoin); @@ -53,13 +57,16 @@ export async function createIssuerAssetCampaign( assetOwner, assetMappedName, assetInitialTokenSupplyWei, - assetWhitelistRequired, + assetWhitelistRequiredForRevenueClaim, + assetWhitelistRequiredForLiquidationClaim, assetName, assetSymbol, assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, @@ -67,7 +74,8 @@ export async function createIssuerAssetCampaign( cfManagerWhitelistRequired, cfManagerInfo, apxRegistry.address, - nameRegistry.address + nameRegistry.address, + campaignFeeManager.address ] ); const receipt = await ethers.provider.waitForTransaction(deployTx.hash); @@ -123,7 +131,9 @@ export async function createIssuerAssetCampaign( assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -160,7 +170,9 @@ export async function createIssuerAssetCampaign( assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, @@ -218,7 +230,9 @@ export async function createIssuerAssetTransferableCampaign( assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -227,6 +241,7 @@ export async function createIssuerAssetTransferableCampaign( cfManagerInfo: String, apxRegistry: String, nameRegistry: String, + campaignFeeManager: String, issuerFactory: Contract, assetTransferableFactory: Contract, cfManagerFactory: Contract, @@ -259,7 +274,9 @@ export async function createIssuerAssetTransferableCampaign( assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, @@ -267,7 +284,8 @@ export async function createIssuerAssetTransferableCampaign( cfManagerWhitelistRequired, cfManagerInfo, apxRegistry, - nameRegistry + nameRegistry, + campaignFeeManager ] ); const receipt = await ethers.provider.waitForTransaction(deployTx.hash); @@ -323,7 +341,9 @@ export async function createAssetTransferableCampaign( assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -360,7 +380,9 @@ export async function createAssetTransferableCampaign( assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, @@ -412,7 +434,9 @@ export async function createAssetSimpleCampaignVesting( assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -446,7 +470,9 @@ export async function createAssetSimpleCampaignVesting( assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, diff --git a/util/helpers.ts b/util/helpers.ts index 7727270..93402bf 100644 --- a/util/helpers.ts +++ b/util/helpers.ts @@ -637,9 +637,16 @@ export async function registerAsset(assetManager: Signer, apxRegistry: Contract, export async function updateState(assetManager: Signer, apxRegistry: Contract, asset: String, state: boolean) { await apxRegistry.connect(assetManager).updateState(asset, state); } -export async function updatePrice(priceManager: Signer, apxRegistry: Contract, asset: Contract, price: Number, expiry: Number) { +export async function updatePrice( + priceManager: Signer, + apxRegistry: Contract, + asset: Contract, + price: Number, + priceDecimals: Number, + expiry: Number +) { const capturedSupply = await asset.totalSupply(); - await apxRegistry.connect(priceManager).updatePrice(asset.address, price, expiry, capturedSupply); + await apxRegistry.connect(priceManager).updatePrice(asset.address, price, priceDecimals, expiry, capturedSupply); } /**