From 9dc62865a0b8b4e65107342d8f166a5e4c4649b7 Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Fri, 11 Feb 2022 13:38:13 +0100 Subject: [PATCH 01/10] wip --- contracts/managers/ACfManager.sol | 49 ++++-- .../CfManagerSoftcapVesting.sol | 99 +++++++----- .../CfManagerSoftcapVestingFactory.sol | 58 +++---- .../ICfManagerSoftcapVestingFactory.sol | 15 +- .../crowdfunding-softcap/CfManagerSoftcap.sol | 95 ++++++----- .../CfManagerSoftcapFactory.sol | 52 +++--- .../ICfManagerSoftcapFactory.sol | 15 +- contracts/services/DeployerService.sol | 151 +++++++++++------- contracts/shared/Structs.sol | 35 ++++ 9 files changed, 339 insertions(+), 230 deletions(-) diff --git a/contracts/managers/ACfManager.sol b/contracts/managers/ACfManager.sol index a6ff0f7..e964b6c 100644 --- a/contracts/managers/ACfManager.sol +++ b/contracts/managers/ACfManager.sol @@ -221,16 +221,21 @@ 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.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.asset, state.stablecoin); + uint256 tokenValue = _token_value(tokens, state.tokenPrice, 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.asset, + state.stablecoin + ); require( totalInvestmentValue >= _adjusted_min_investment(floatingTokens), "ACfManager: Investment amount too low." @@ -297,8 +302,12 @@ abstract contract ACfManager is IVersioned, IACfManager { 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, + address asset, + address stable + ) internal view returns (uint256) { return tokens * tokenPrice * _stablecoin_decimals_precision(stable) @@ -311,7 +320,7 @@ 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.asset, state.stablecoin); return (remainingTokensValue < state.minInvestment) ? remainingTokensValue : state.minInvestment; } @@ -319,21 +328,39 @@ abstract contract ACfManager is IVersioned, IACfManager { uint256 tokenAmountForInvestment = _token_amount_for_investment( state.softCap - state.totalFundsRaised, state.tokenPrice, - state.asset + state.asset, + state.stablecoin ); - return _token_value(tokenAmountForInvestment, state.tokenPrice, state.asset); + return _token_value(tokenAmountForInvestment, state.tokenPrice, state.asset, state.stablecoin); } function _token_amount_for_investment( uint256 investment, uint256 tokenPrice, - address asset + address asset, + address stable ) internal view returns (uint256) { - address stable = IIssuerCommon(IAssetCommon(asset).commonState().issuer).commonState().stablecoin; return investment * _asset_price_precision(asset) * _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); } + } + + function _safe_price_precision_fetch(address asset) internal view returns (uint256) { + (bool success, bytes memory result) = asset.staticcall(abi.encodeWithSignature("priceDecimalsPrecision()")); + if (success) { + return abi.decode(result, (uint256)); + } else { return 0; } + } } diff --git a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol index 70ae9dc..573c2eb 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol @@ -48,57 +48,80 @@ 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 != address(0), "CfManagerSoftcapVesting: Invalid issuer."); + + uint256 fetchedPricePrecision = _safe_price_precision_fetch(params.asset); + uint256 pricePrecisionProcessed = fetchedPricePrecision > 0 ? fetchedPricePrecision : params.tokenPricePrecision; + require(params.tokenPricePrecision > 0, "CfManagerSoftcapVesting: Invalid price precision."); + + address paymentMethodProcessed = params.paymentMethod == address(0) ? + IIssuerCommon(issuerProcessed).commonState().stablecoin : + params.paymentMethod; uint256 softCapNormalized = _token_value( - _token_amount_for_investment(softCap, tokenPrice, asset), - tokenPrice, - asset + _token_amount_for_investment( + params.softCap, + params.tokenPrice, + params.asset, + paymentMethodProcessed + ), + params.tokenPrice, + params.asset, + paymentMethodProcessed ); uint256 minInvestmentNormalized = _token_value( - _token_amount_for_investment(minInvestment, tokenPrice, asset), - tokenPrice, - asset + _token_amount_for_investment( + params.minInvestment, + params.tokenPrice, + params.asset, + paymentMethodProcessed + ), + params.tokenPrice, + params.asset, + paymentMethodProcessed ); state = Structs.CfManagerState( - contractFlavor, - contractVersion, + params.contractFlavor, + params.contractVersion, address(this), - owner, - asset, - address(issuer), - issuer.commonState().stablecoin, - tokenPrice, + params.owner, + params.asset, + address(params.issuer), + issuerProcessed, + params.tokenPrice, + pricePrecisionProcessed, 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.asset, + paymentMethodProcessed + ) >= softCapNormalized, "CfManagerSoftcapVesting: Invalid soft cap." ); } @@ -119,7 +142,7 @@ 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.asset, state.stablecoin); require(unreleased > 0, "CfManagerSoftcapVesting: No tokens to be released."); state.totalClaimableTokens -= unreleased; diff --git a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol index a3ab756..8e42abc 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.paymentMethod, + params.initialPricePerToken, + params.tokenPricePrecision, + 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..0867a45 100644 --- a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol +++ b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol @@ -21,56 +21,77 @@ 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 != address(0), "CfManagerSoftcap: Invalid issuer."); + + uint256 fetchedPricePrecision = _safe_price_precision_fetch(params.asset); + uint256 pricePrecisionProcessed = fetchedPricePrecision > 0 ? fetchedPricePrecision : params.tokenPricePrecision; + require(params.tokenPricePrecision > 0, "CfManagerSoftcap: Invalid price precision."); + + address paymentMethodProcessed = params.paymentMethod == address(0) ? + IIssuerCommon(issuerProcessed).commonState().stablecoin : + params.paymentMethod; uint256 softCapNormalized = _token_value( - _token_amount_for_investment(softCap, tokenPrice, asset), - tokenPrice, - asset + _token_amount_for_investment( + params.softCap, + params.tokenPrice, + params.asset, + paymentMethodProcessed + ), + params.tokenPrice, + params.asset, + paymentMethodProcessed ); uint256 minInvestmentNormalized = _token_value( - _token_amount_for_investment(minInvestment, tokenPrice, asset), - tokenPrice, - asset + _token_amount_for_investment( + params.minInvestment, + params.tokenPrice, + params.asset, + paymentMethodProcessed + ), + params.tokenPrice, + params.asset, + paymentMethodProcessed ); - 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, + paymentMethodProcessed, + params.tokenPrice, + pricePrecisionProcessed, 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.asset, + paymentMethodProcessed + ) >= softCapNormalized, "CfManagerSoftcap: Invalid soft cap." ); } diff --git a/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol b/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol index d8b62c0..8076508 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.paymentMethod, + params.initialPricePerToken, + params.tokenPricePrecision, + 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/services/DeployerService.sol b/contracts/services/DeployerService.sol index fdb549c..4f1d144 100644 --- a/contracts/services/DeployerService.sol +++ b/contracts/services/DeployerService.sol @@ -215,18 +215,24 @@ contract DeployerService is IVersioned { 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(0), // used only when creating campaign for preixisting assets + address(0), // used only when creating campaignn for preixisting assets + request.cfManagerPricePerToken, + 0, // used only when creating campaignn for preixisting assets + request.cfManagerSoftcap, + request.cfManagerSoftcapMinInvestment, + request.cfManagerSoftcapMaxInvestment, + request.cfManagerWhitelistRequired, + request.cfManagerInfo, + request.nameRegistry, + request.feeManager + ) )); // Whitelist owners @@ -268,18 +274,24 @@ contract DeployerService is IVersioned { 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(0), // used only when creating campaign for preixisting assets + address(0), // used only when creating campaign for preixisting assets + request.cfManagerPricePerToken, + 0, // used only when creating campaign for preixisting assets + 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 @@ -327,18 +339,25 @@ contract DeployerService is IVersioned { ) ); - 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(0), // used only when creating campaign for preixisting assets + address(0), // used only when creating campaign for preixisting assets + request.cfManagerPricePerToken, + 0, // used only when creating campaign for preixisting assets + request.cfManagerSoftcap, + request.cfManagerSoftcapMinInvestment, + request.cfManagerSoftcapMaxInvestment, + request.cfManagerWhitelistRequired, + request.cfManagerInfo, + request.nameRegistry, + request.feeManager + ) + )); // Whitelist issuer owner @@ -384,18 +403,24 @@ contract DeployerService is IVersioned { 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(0), // used only when creating campaign for preixisting assets + address(0), // used only when creating campaign for preixisting assets + request.cfManagerPricePerToken, + 0, // used only when creating campaign for preixisting assets + 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 @@ -428,18 +453,24 @@ contract DeployerService is IVersioned { ) ) ); - 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), + address(0), // used only when creating campaign for preixisting assets + address(0), // used only when creating campaign for preixisting assets + request.cfManagerPricePerToken, + 0, // used only when creating campaign for preixisting assets + 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 diff --git a/contracts/shared/Structs.sol b/contracts/shared/Structs.sol index 9d4f4cd..689655c 100644 --- a/contracts/shared/Structs.sol +++ b/contracts/shared/Structs.sol @@ -35,6 +35,40 @@ contract Structs { AssetCommonState asset; string mappedName; } + + struct CampaignFactoryParams { + address owner; + string mappedName; + address assetAddress; + address issuerAddress; + address paymentMethod; + uint256 initialPricePerToken; + uint256 tokenPricePrecision; + 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 paymentMethod; + uint256 tokenPrice; + uint256 tokenPricePrecision; + uint256 softCap; + uint256 minInvestment; + uint256 maxInvestment; + bool whitelistRequired; + string info; + address feeManager; + } struct CampaignCommonState { string flavor; @@ -272,6 +306,7 @@ contract Structs { address issuer; address stablecoin; uint256 tokenPrice; + uint256 tokenPricePrecision; uint256 softCap; uint256 minInvestment; uint256 maxInvestment; From b8fab2cd5f08cf553e4882df53a62e17346ca55e Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Sun, 27 Feb 2022 19:11:37 +0100 Subject: [PATCH 02/10] fix bugs; tests working --- .../CfManagerSoftcapVesting.sol | 4 ++-- .../managers/crowdfunding-softcap/CfManagerSoftcap.sol | 2 +- test/campaign/campaign-deployer.ts | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol index 573c2eb..5d21d4c 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol @@ -67,7 +67,7 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { uint256 fetchedPricePrecision = _safe_price_precision_fetch(params.asset); uint256 pricePrecisionProcessed = fetchedPricePrecision > 0 ? fetchedPricePrecision : params.tokenPricePrecision; - require(params.tokenPricePrecision > 0, "CfManagerSoftcapVesting: Invalid price precision."); + require(pricePrecisionProcessed > 0, "CfManagerSoftcapVesting: Invalid price precision."); address paymentMethodProcessed = params.paymentMethod == address(0) ? IIssuerCommon(issuerProcessed).commonState().stablecoin : @@ -100,8 +100,8 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { address(this), params.owner, params.asset, - address(params.issuer), issuerProcessed, + paymentMethodProcessed, params.tokenPrice, pricePrecisionProcessed, softCapNormalized, diff --git a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol index 0867a45..47b606a 100644 --- a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol +++ b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol @@ -37,7 +37,7 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { uint256 fetchedPricePrecision = _safe_price_precision_fetch(params.asset); uint256 pricePrecisionProcessed = fetchedPricePrecision > 0 ? fetchedPricePrecision : params.tokenPricePrecision; - require(params.tokenPricePrecision > 0, "CfManagerSoftcap: Invalid price precision."); + require(pricePrecisionProcessed > 0, "CfManagerSoftcap: Invalid price precision."); address paymentMethodProcessed = params.paymentMethod == address(0) ? IIssuerCommon(issuerProcessed).commonState().stablecoin : diff --git a/test/campaign/campaign-deployer.ts b/test/campaign/campaign-deployer.ts index 41d8204..0df4167 100644 --- a/test/campaign/campaign-deployer.ts +++ b/test/campaign/campaign-deployer.ts @@ -16,11 +16,14 @@ export async function createCampaign( nameRegistry: Contract, feeRegistry: Contract ): Promise { - const cfManagerTx = await cfManagerFactory.create( + const cfManagerTx = await cfManagerFactory.create([ owner, mappedName, asset.address, + ethers.constants.AddressZero, + ethers.constants.AddressZero, pricePerToken, + 0, softCapWei, minInvestmentWei, maxInvestmentWei, @@ -28,7 +31,7 @@ export async function createCampaign( info, nameRegistry.address, feeRegistry.address - ); + ]); const receipt = await ethers.provider.waitForTransaction(cfManagerTx.hash); for (const log of receipt.logs) { try { From 4615b2b8c5f4a2b878889ad7d35cf5a13ddb76dd Mon Sep 17 00:00:00 2001 From: Filip Dujmusic <38497167+filipdujmusic@users.noreply.github.com> Date: Wed, 2 Mar 2022 10:54:28 +0100 Subject: [PATCH 03/10] decouple campaign price from asset state (#47) * decouple campaign price from asset state removed the tokenPricePrecision property from assets use priceDecimals rather than price precision (use 4, not 10**4) provide priceDecimals as a parameter while creating campaign provide priceDecimals as a parameter while updateing price on ApxRegistry (important for liquidations) track priceDecimals when storing the highestSellPrice on asset state (important for liquidations) make all the _token_value() methods look the same (operations ordering) fix tests add priceDecimalsPrecision to the campaign common state fix deployer service scripts to take into account new priceDecimals parameter when creating campaigns * PR fixes * PR fixes --- contracts/apx-protocol/ApxAssetsRegistry.sol | 15 +++- contracts/apx-protocol/IApxAssetsRegistry.sol | 1 + contracts/asset-simple/AssetSimple.sol | 5 -- .../asset-transferable/AssetTransferable.sol | 66 ++++++++++---- contracts/asset/Asset.sol | 59 +++++++++--- contracts/managers/ACfManager.sol | 90 +++++++++++++------ .../CfManagerSoftcapVesting.sol | 35 +++++--- .../CfManagerSoftcapVestingFactory.sol | 4 +- .../crowdfunding-softcap/CfManagerSoftcap.sol | 27 +++--- .../CfManagerSoftcapFactory.sol | 4 +- contracts/services/DeployerService.sol | 15 ++-- contracts/services/QueryService.sol | 5 +- contracts/shared/IAssetCommon.sol | 1 - contracts/shared/Structs.sol | 14 +-- test/TestData.ts | 6 +- .../asset-transferable-full-flow.ts | 2 +- test/asset/asset-full-flow.ts | 2 +- util/deployer-service.ts | 6 ++ util/helpers.ts | 11 ++- 19 files changed, 252 insertions(+), 116 deletions(-) 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/managers/ACfManager.sol b/contracts/managers/ACfManager.sol index e964b6c..de45583 100644 --- a/contracts/managers/ACfManager.sol +++ b/contracts/managers/ACfManager.sol @@ -140,16 +140,8 @@ 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); } @@ -178,6 +170,7 @@ abstract contract ACfManager is IVersioned, IACfManager { state.finalized, state.canceled, state.tokenPrice, + state.tokenPriceDecimals, state.totalFundsRaised, state.totalTokensSold ); @@ -221,18 +214,37 @@ 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.stablecoin) >= 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, state.stablecoin); - uint256 tokenValue = _token_value(tokens, state.tokenPrice, state.asset, state.stablecoin); + 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.tokenPriceDecimals, state.asset, state.stablecoin ); @@ -277,6 +289,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)) @@ -294,10 +324,6 @@ 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(); } @@ -305,13 +331,14 @@ abstract contract ACfManager is IVersioned, IACfManager { 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); } @@ -320,7 +347,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, state.stablecoin); + uint256 remainingTokensValue = _token_value( + remainingTokens, + state.tokenPrice, + state.tokenPriceDecimals, + state.asset, + state.stablecoin + ); return (remainingTokensValue < state.minInvestment) ? remainingTokensValue : state.minInvestment; } @@ -328,20 +361,28 @@ abstract contract ACfManager is IVersioned, IACfManager { uint256 tokenAmountForInvestment = _token_amount_for_investment( state.softCap - state.totalFundsRaised, state.tokenPrice, + 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, state.stablecoin); } function _token_amount_for_investment( uint256 investment, uint256 tokenPrice, + uint8 tokenPriceDecimals, address asset, address stable ) internal view returns (uint256) { return investment - * _asset_price_precision(asset) + * (10 ** tokenPriceDecimals) * _asset_decimals_precision(asset) / tokenPrice / _stablecoin_decimals_precision(stable); @@ -356,11 +397,4 @@ abstract contract ACfManager is IVersioned, IACfManager { return assetCommonState.issuer; } else { return address(0); } } - - function _safe_price_precision_fetch(address asset) internal view returns (uint256) { - (bool success, bytes memory result) = asset.staticcall(abi.encodeWithSignature("priceDecimalsPrecision()")); - if (success) { - return abi.decode(result, (uint256)); - } else { return 0; } - } } diff --git a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol index 5d21d4c..95e2457 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol @@ -65,34 +65,34 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { address issuerProcessed = fetchedIssuer != address(0) ? fetchedIssuer : params.issuer; require(issuerProcessed != address(0), "CfManagerSoftcapVesting: Invalid issuer."); - uint256 fetchedPricePrecision = _safe_price_precision_fetch(params.asset); - uint256 pricePrecisionProcessed = fetchedPricePrecision > 0 ? fetchedPricePrecision : params.tokenPricePrecision; - require(pricePrecisionProcessed > 0, "CfManagerSoftcapVesting: Invalid price precision."); - - address paymentMethodProcessed = params.paymentMethod == address(0) ? + address paymentTokenProcessed = params.paymentToken == address(0) ? IIssuerCommon(issuerProcessed).commonState().stablecoin : - params.paymentMethod; + params.paymentToken; uint256 softCapNormalized = _token_value( _token_amount_for_investment( params.softCap, params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ), params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ); uint256 minInvestmentNormalized = _token_value( _token_amount_for_investment( params.minInvestment, params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ), params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ); state = Structs.CfManagerState( params.contractFlavor, @@ -101,9 +101,9 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { params.owner, params.asset, issuerProcessed, - paymentMethodProcessed, + paymentTokenProcessed, params.tokenPrice, - pricePrecisionProcessed, + params.tokenPriceDecimals, softCapNormalized, minInvestmentNormalized, params.maxInvestment, @@ -119,8 +119,9 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { _token_value( IToken(params.asset).totalSupply(), params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ) >= softCapNormalized, "CfManagerSoftcapVesting: Invalid soft cap." ); @@ -142,7 +143,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, state.stablecoin); + 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; diff --git a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol index 8e42abc..0349487 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVestingFactory.sol @@ -43,9 +43,9 @@ contract CfManagerSoftcapVestingFactory is ICfManagerSoftcapVestingFactory { params.owner, params.assetAddress, params.issuerAddress, - params.paymentMethod, + params.paymentToken, params.initialPricePerToken, - params.tokenPricePrecision, + params.tokenPriceDecimals, params.softCap, params.minInvestment, params.maxInvestment, diff --git a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol index 47b606a..71f51db 100644 --- a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol +++ b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol @@ -35,34 +35,34 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { address issuerProcessed = fetchedIssuer != address(0) ? fetchedIssuer : params.issuer; require(issuerProcessed != address(0), "CfManagerSoftcap: Invalid issuer."); - uint256 fetchedPricePrecision = _safe_price_precision_fetch(params.asset); - uint256 pricePrecisionProcessed = fetchedPricePrecision > 0 ? fetchedPricePrecision : params.tokenPricePrecision; - require(pricePrecisionProcessed > 0, "CfManagerSoftcap: Invalid price precision."); - - address paymentMethodProcessed = params.paymentMethod == address(0) ? + address paymentTokenProcessed = params.paymentToken == address(0) ? IIssuerCommon(issuerProcessed).commonState().stablecoin : - params.paymentMethod; + params.paymentToken; uint256 softCapNormalized = _token_value( _token_amount_for_investment( params.softCap, params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ), params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ); uint256 minInvestmentNormalized = _token_value( _token_amount_for_investment( params.minInvestment, params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ), params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ); state = Structs.CfManagerState( @@ -72,9 +72,9 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { params.owner, params.asset, issuerProcessed, - paymentMethodProcessed, + paymentTokenProcessed, params.tokenPrice, - pricePrecisionProcessed, + params.tokenPriceDecimals, softCapNormalized, minInvestmentNormalized, params.maxInvestment, @@ -89,8 +89,9 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { _token_value( IToken(params.asset).totalSupply(), params.tokenPrice, + params.tokenPriceDecimals, params.asset, - paymentMethodProcessed + paymentTokenProcessed ) >= softCapNormalized, "CfManagerSoftcap: Invalid soft cap." ); diff --git a/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol b/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol index 8076508..c919ae2 100644 --- a/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol +++ b/contracts/managers/crowdfunding-softcap/CfManagerSoftcapFactory.sol @@ -41,9 +41,9 @@ contract CfManagerSoftcapFactory is ICfManagerSoftcapFactory { params.owner, params.assetAddress, params.issuerAddress, - params.paymentMethod, + params.paymentToken, params.initialPricePerToken, - params.tokenPricePrecision, + params.tokenPriceDecimals, params.softCap, params.minInvestment, params.maxInvestment, diff --git a/contracts/services/DeployerService.sol b/contracts/services/DeployerService.sol index 67c112d..75dfab3 100644 --- a/contracts/services/DeployerService.sol +++ b/contracts/services/DeployerService.sol @@ -66,6 +66,7 @@ interface IDeployerService is IVersioned { address cfManagerOwner; string cfManagerMappedName; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -92,6 +93,7 @@ interface IDeployerService is IVersioned { address cfManagerOwner; string cfManagerMappedName; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -123,6 +125,7 @@ interface IDeployerService is IVersioned { address cfManagerOwner; string cfManagerMappedName; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -149,6 +152,7 @@ interface IDeployerService is IVersioned { address cfManagerOwner; string cfManagerMappedName; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -173,6 +177,7 @@ interface IDeployerService is IVersioned { address cfManagerOwner; string cfManagerMappedName; uint256 cfManagerPricePerToken; + uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; uint256 cfManagerSoftcapMinInvestment; uint256 cfManagerSoftcapMaxInvestment; @@ -235,7 +240,7 @@ contract DeployerService is IDeployerService { address(0), // used only when creating campaign for preixisting assets address(0), // used only when creating campaignn for preixisting assets request.cfManagerPricePerToken, - 0, // used only when creating campaignn for preixisting assets + request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, request.cfManagerSoftcapMinInvestment, request.cfManagerSoftcapMaxInvestment, @@ -294,7 +299,7 @@ contract DeployerService is IDeployerService { address(0), // used only when creating campaign for preixisting assets address(0), // used only when creating campaign for preixisting assets request.cfManagerPricePerToken, - 0, // used only when creating campaign for preixisting assets + request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, request.cfManagerSoftcapMinInvestment, request.cfManagerSoftcapMaxInvestment, @@ -359,7 +364,7 @@ contract DeployerService is IDeployerService { address(0), // used only when creating campaign for preixisting assets address(0), // used only when creating campaign for preixisting assets request.cfManagerPricePerToken, - 0, // used only when creating campaign for preixisting assets + request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, request.cfManagerSoftcapMinInvestment, request.cfManagerSoftcapMaxInvestment, @@ -423,7 +428,7 @@ contract DeployerService is IDeployerService { address(0), // used only when creating campaign for preixisting assets address(0), // used only when creating campaign for preixisting assets request.cfManagerPricePerToken, - 0, // used only when creating campaign for preixisting assets + request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, request.cfManagerSoftcapMinInvestment, request.cfManagerSoftcapMaxInvestment, @@ -473,7 +478,7 @@ contract DeployerService is IDeployerService { address(0), // used only when creating campaign for preixisting assets address(0), // used only when creating campaign for preixisting assets request.cfManagerPricePerToken, - 0, // used only when creating campaign for preixisting assets + request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, request.cfManagerSoftcapMinInvestment, request.cfManagerSoftcapMaxInvestment, 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 b33a4f1..5e5a1ce 100644 --- a/contracts/shared/Structs.sol +++ b/contracts/shared/Structs.sol @@ -50,9 +50,9 @@ contract Structs { string mappedName; address assetAddress; address issuerAddress; - address paymentMethod; + address paymentToken; uint256 initialPricePerToken; - uint256 tokenPricePrecision; + uint8 tokenPriceDecimals; uint256 softCap; uint256 minInvestment; uint256 maxInvestment; @@ -68,9 +68,9 @@ contract Structs { address owner; address asset; address issuer; - address paymentMethod; + address paymentToken; uint256 tokenPrice; - uint256 tokenPricePrecision; + uint8 tokenPriceDecimals; uint256 softCap; uint256 minInvestment; uint256 maxInvestment; @@ -91,6 +91,7 @@ contract Structs { bool finalized; bool canceled; uint256 pricePerToken; + uint8 tokenPriceDecimals; uint256 fundsRaised; uint256 tokensSold; } @@ -121,6 +122,7 @@ contract Structs { bool state; uint256 stateUpdatedAt; uint256 price; + uint8 priceDecimals; uint256 priceUpdatedAt; uint256 priceValidUntil; uint256 capturedSupply; @@ -249,6 +251,7 @@ contract Structs { uint256 totalAmountRaised; uint256 totalTokensSold; uint256 highestTokenSellPrice; + uint8 highestTokenSellPriceDecimals; uint256 totalTokensLocked; uint256 totalTokensLockedAndLiquidated; bool liquidated; @@ -274,6 +277,7 @@ contract Structs { uint256 totalAmountRaised; uint256 totalTokensSold; uint256 highestTokenSellPrice; + uint8 highestTokenSellPriceDecimals; bool liquidated; uint256 liquidationFundsTotal; uint256 liquidationTimestamp; @@ -308,7 +312,7 @@ contract Structs { address issuer; address stablecoin; uint256 tokenPrice; - uint256 tokenPricePrecision; + uint8 tokenPriceDecimals; uint256 softCap; uint256 minInvestment; uint256 maxInvestment; diff --git a/test/TestData.ts b/test/TestData.ts index d5b28e2..b8fa4b7 100644 --- a/test/TestData.ts +++ b/test/TestData.ts @@ -68,6 +68,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 @@ -158,6 +159,7 @@ export class TestData { issuerOwnerAddress, this.campaignAnsName, this.campaignInitialPricePerToken, + this.campaignTokenPriceDecimals, this.campaignSoftCap, this.campaignMinInvestment, this.campaignMaxInvestment, @@ -193,6 +195,7 @@ export class TestData { issuerOwnerAddress, this.campaignAnsName, this.campaignInitialPricePerToken, + this.campaignTokenPriceDecimals, this.campaignSoftCap, this.campaignMinInvestment, this.campaignMaxInvestment, @@ -227,6 +230,7 @@ export class TestData { issuerOwnerAddress, this.campaignAnsName, this.campaignInitialPricePerToken, + this.campaignTokenPriceDecimals, this.campaignSoftCap, this.campaignMinInvestment, this.campaignMaxInvestment, @@ -265,7 +269,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 d0102ac..72c1c15 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 b1225d2..627aaa9 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/util/deployer-service.ts b/util/deployer-service.ts index d52c990..afb5d28 100644 --- a/util/deployer-service.ts +++ b/util/deployer-service.ts @@ -122,6 +122,7 @@ export async function createIssuerAssetCampaign( cfManagerOwner: String, cfManagerMappedName: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -158,6 +159,7 @@ export async function createIssuerAssetCampaign( cfManagerOwner, cfManagerMappedName, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, @@ -320,6 +322,7 @@ export async function createAssetTransferableCampaign( cfManagerOwner: String, cfManagerMappedName: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -356,6 +359,7 @@ export async function createAssetTransferableCampaign( cfManagerOwner, cfManagerMappedName, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, @@ -408,6 +412,7 @@ export async function createAssetSimpleCampaignVesting( cfManagerOwner: String, cfManagerMappedName: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -441,6 +446,7 @@ export async function createAssetSimpleCampaignVesting( cfManagerOwner, cfManagerMappedName, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, diff --git a/util/helpers.ts b/util/helpers.ts index df5242f..eb249c8 100644 --- a/util/helpers.ts +++ b/util/helpers.ts @@ -612,9 +612,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); } /** From 80c9445cec69f0c2cdd389493ee55a1a97effe7d Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Fri, 4 Mar 2022 17:27:33 +0100 Subject: [PATCH 04/10] fix tests --- .../CfManagerSoftcapVesting.sol | 2 +- .../crowdfunding-softcap/CfManagerSoftcap.sol | 2 +- contracts/services/DeployerService.sol | 25 +++++++++++-------- test/TestData.ts | 3 +++ util/deployer-service.ts | 18 +++++++++++-- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol index 95e2457..aca570a 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol @@ -63,7 +63,7 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { address fetchedIssuer = _safe_issuer_fetch(params.asset); address issuerProcessed = fetchedIssuer != address(0) ? fetchedIssuer : params.issuer; - require(issuerProcessed != address(0), "CfManagerSoftcapVesting: Invalid issuer."); + require(issuerProcessed == params.issuer, "CfManagerSoftcapVesting: Invalid issuer provided."); address paymentTokenProcessed = params.paymentToken == address(0) ? IIssuerCommon(issuerProcessed).commonState().stablecoin : diff --git a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol index 71f51db..3fd088e 100644 --- a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol +++ b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol @@ -33,7 +33,7 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { address fetchedIssuer = _safe_issuer_fetch(params.asset); address issuerProcessed = fetchedIssuer != address(0) ? fetchedIssuer : params.issuer; - require(issuerProcessed != address(0), "CfManagerSoftcap: Invalid issuer."); + require(issuerProcessed == params.issuer, "CfManagerSoftcap: Invalid issuer provided."); address paymentTokenProcessed = params.paymentToken == address(0) ? IIssuerCommon(issuerProcessed).commonState().stablecoin : diff --git a/contracts/services/DeployerService.sol b/contracts/services/DeployerService.sol index 75dfab3..065243c 100644 --- a/contracts/services/DeployerService.sol +++ b/contracts/services/DeployerService.sol @@ -65,6 +65,7 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; @@ -92,6 +93,7 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; @@ -124,6 +126,7 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; @@ -151,6 +154,7 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; @@ -176,6 +180,7 @@ interface IDeployerService is IVersioned { string assetInfo; address cfManagerOwner; string cfManagerMappedName; + address cfManagerPaymentToken; uint256 cfManagerPricePerToken; uint8 cfManagerTokenPriceDecimals; uint256 cfManagerSoftcap; @@ -237,8 +242,8 @@ contract DeployerService is IDeployerService { address(this), request.cfManagerMappedName, address(asset), - address(0), // used only when creating campaign for preixisting assets - address(0), // used only when creating campaignn for preixisting assets + address(issuer), + request.cfManagerPaymentToken, request.cfManagerPricePerToken, request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, @@ -296,8 +301,8 @@ contract DeployerService is IDeployerService { address(this), request.cfManagerMappedName, address(asset), - address(0), // used only when creating campaign for preixisting assets - address(0), // used only when creating campaign for preixisting assets + request.issuer, + request.cfManagerPaymentToken, request.cfManagerPricePerToken, request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, @@ -361,8 +366,8 @@ contract DeployerService is IDeployerService { address(this), request.cfManagerMappedName, address(asset), - address(0), // used only when creating campaign for preixisting assets - address(0), // used only when creating campaign for preixisting assets + address(issuer), + request.cfManagerPaymentToken, request.cfManagerPricePerToken, request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, @@ -425,8 +430,8 @@ contract DeployerService is IDeployerService { address(this), request.cfManagerMappedName, address(asset), - address(0), // used only when creating campaign for preixisting assets - address(0), // used only when creating campaign for preixisting assets + request.issuer, + request.cfManagerPaymentToken, request.cfManagerPricePerToken, request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, @@ -475,8 +480,8 @@ contract DeployerService is IDeployerService { address(this), request.cfManagerMappedName, address(asset), - address(0), // used only when creating campaign for preixisting assets - address(0), // used only when creating campaign for preixisting assets + request.issuer, + request.cfManagerPaymentToken, request.cfManagerPricePerToken, request.cfManagerTokenPriceDecimals, request.cfManagerSoftcap, diff --git a/test/TestData.ts b/test/TestData.ts index b8fa4b7..a013fa6 100644 --- a/test/TestData.ts +++ b/test/TestData.ts @@ -158,6 +158,7 @@ export class TestData { this.assetInfoHash, issuerOwnerAddress, this.campaignAnsName, + this.stablecoin.address, this.campaignInitialPricePerToken, this.campaignTokenPriceDecimals, this.campaignSoftCap, @@ -194,6 +195,7 @@ export class TestData { this.assetInfoHash, issuerOwnerAddress, this.campaignAnsName, + this.stablecoin.address, this.campaignInitialPricePerToken, this.campaignTokenPriceDecimals, this.campaignSoftCap, @@ -229,6 +231,7 @@ export class TestData { this.assetInfoHash, issuerOwnerAddress, this.campaignAnsName, + this.stablecoin.address, this.campaignInitialPricePerToken, this.campaignTokenPriceDecimals, this.campaignSoftCap, diff --git a/util/deployer-service.ts b/util/deployer-service.ts index afb5d28..bb6b904 100644 --- a/util/deployer-service.ts +++ b/util/deployer-service.ts @@ -12,12 +12,14 @@ 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, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, @@ -51,12 +53,14 @@ export async function createIssuerAssetCampaign( assetOwner, assetMappedName, assetInitialTokenSupplyWei, - assetWhitelistRequired, + assetWhitelistRequiredForRevenueClaim, + assetWhitelistRequiredForLiquidationClaim, assetName, assetSymbol, assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, cfManagerSoftcapWei, cfManagerMinInvestmentWei, @@ -121,6 +125,7 @@ export async function createIssuerAssetCampaign( assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, @@ -158,6 +163,7 @@ export async function createIssuerAssetCampaign( assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, cfManagerTokenPriceDecimals, cfManagerSoftcapWei, @@ -217,7 +223,9 @@ export async function createIssuerAssetTransferableCampaign( assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -257,7 +265,9 @@ export async function createIssuerAssetTransferableCampaign( assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, @@ -321,6 +331,7 @@ export async function createAssetTransferableCampaign( assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, @@ -358,6 +369,7 @@ export async function createAssetTransferableCampaign( assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, cfManagerTokenPriceDecimals, cfManagerSoftcapWei, @@ -411,6 +423,7 @@ export async function createAssetSimpleCampaignVesting( assetInfo: String, cfManagerOwner: String, cfManagerMappedName: String, + cfManagerPaymentToken: String, cfManagerPricePerToken: Number, cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, @@ -445,6 +458,7 @@ export async function createAssetSimpleCampaignVesting( assetInfo, cfManagerOwner, cfManagerMappedName, + cfManagerPaymentToken, cfManagerPricePerToken, cfManagerTokenPriceDecimals, cfManagerSoftcapWei, From eb5549b748b6ccbf3f5a08e79732be67d3cd3db3 Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Sat, 5 Mar 2022 17:07:29 +0100 Subject: [PATCH 05/10] minor fixes --- util/deployer-service.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/util/deployer-service.ts b/util/deployer-service.ts index bb6b904..cbff77f 100644 --- a/util/deployer-service.ts +++ b/util/deployer-service.ts @@ -21,6 +21,7 @@ export async function createIssuerAssetCampaign( cfManagerMappedName: String, cfManagerPaymentToken: String, cfManagerPricePerToken: Number, + cfManagerTokenPriceDecimals: Number, cfManagerSoftcap: Number, cfManagerMinInvestment: Number, cfManagerMaxInvestment: Number, @@ -32,7 +33,8 @@ export async function createIssuerAssetCampaign( cfManagerFactory: Contract, deployerService: Contract, apxRegistry: Contract, - nameRegistry: Contract + nameRegistry: Contract, + campaignFeeManager: Contract ): Promise> { const stablecoin = await ethers.getContractAt("USDC", issuerStablecoin); const assetInitialTokenSupplyWei = ethers.utils.parseEther(assetInitialTokenSupply.toString()); @@ -62,6 +64,7 @@ export async function createIssuerAssetCampaign( cfManagerMappedName, cfManagerPaymentToken, cfManagerPricePerToken, + cfManagerTokenPriceDecimals, cfManagerSoftcapWei, cfManagerMinInvestmentWei, cfManagerMaxInvestmentWei, @@ -69,7 +72,8 @@ export async function createIssuerAssetCampaign( cfManagerWhitelistRequired, cfManagerInfo, apxRegistry.address, - nameRegistry.address + nameRegistry.address, + campaignFeeManager.address ] ); const receipt = await ethers.provider.waitForTransaction(deployTx.hash); @@ -234,6 +238,7 @@ export async function createIssuerAssetTransferableCampaign( cfManagerInfo: String, apxRegistry: String, nameRegistry: String, + campaignFeeManager: String, issuerFactory: Contract, assetTransferableFactory: Contract, cfManagerFactory: Contract, @@ -275,7 +280,8 @@ export async function createIssuerAssetTransferableCampaign( cfManagerWhitelistRequired, cfManagerInfo, apxRegistry, - nameRegistry + nameRegistry, + campaignFeeManager ] ); const receipt = await ethers.provider.waitForTransaction(deployTx.hash); From 9341db77af56b8f5e496a5d5561d34aac3ea992b Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Mon, 7 Mar 2022 08:26:35 +0100 Subject: [PATCH 06/10] fix tests --- test/campaign/campaign-deployer.ts | 3 ++- test/campaign/campaign-tests.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/campaign/campaign-deployer.ts b/test/campaign/campaign-deployer.ts index 0df4167..f826b52 100644 --- a/test/campaign/campaign-deployer.ts +++ b/test/campaign/campaign-deployer.ts @@ -6,6 +6,7 @@ export async function createCampaign( owner: String, mappedName: String, asset: Contract, + issuer: Contract, pricePerToken: Number, softCapWei: Number, minInvestmentWei: Number, @@ -20,7 +21,7 @@ export async function createCampaign( owner, mappedName, asset.address, - ethers.constants.AddressZero, + issuer.address, ethers.constants.AddressZero, pricePerToken, 0, diff --git a/test/campaign/campaign-tests.ts b/test/campaign/campaign-tests.ts index e90dfe8..cf083b7 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 From 83d2f7affcf1a9c0e783ae57d51eef59c24a1bce Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Mon, 18 Jul 2022 12:00:33 +0200 Subject: [PATCH 07/10] make campaing managers ownable (oz standard) --- contracts/managers/ACfManager.sol | 25 +++++-------------- contracts/managers/IACfManager.sol | 1 - .../CfManagerSoftcapVesting.sol | 13 +++++++--- .../crowdfunding-softcap/CfManagerSoftcap.sol | 10 ++++++-- contracts/services/DeployerService.sol | 11 ++++---- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/contracts/managers/ACfManager.sol b/contracts/managers/ACfManager.sol index de45583..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( @@ -146,7 +138,7 @@ abstract contract ACfManager is IVersioned, IACfManager { 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); } @@ -162,7 +154,7 @@ abstract contract ACfManager is IVersioned, IACfManager { state.flavor, state.version, state.contractAddress, - state.owner, + owner(), state.info, state.asset, state.stablecoin, @@ -181,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 @@ -194,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)); } 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 aca570a..25ebf37 100644 --- a/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol +++ b/contracts/managers/crowdfunding-softcap-vesting/CfManagerSoftcapVesting.sol @@ -64,6 +64,12 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { 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 : @@ -125,6 +131,7 @@ contract CfManagerSoftcapVesting is ICfManagerSoftcapVesting, ACfManager { ) >= softCapNormalized, "CfManagerSoftcapVesting: Invalid soft cap." ); + _transferOwnership(params.owner); } //------------------------ @@ -163,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"); @@ -182,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."); @@ -205,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/CfManagerSoftcap.sol b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol index 3fd088e..386b950 100644 --- a/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol +++ b/contracts/managers/crowdfunding-softcap/CfManagerSoftcap.sol @@ -30,10 +30,15 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { "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 : @@ -95,6 +100,7 @@ contract CfManagerSoftcap is ICfManagerSoftcap, ACfManager { ) >= softCapNormalized, "CfManagerSoftcap: Invalid soft cap." ); + _transferOwnership(params.owner); } //------------------------ @@ -122,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/services/DeployerService.sol b/contracts/services/DeployerService.sol index 065243c..324431e 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"; @@ -272,7 +273,7 @@ contract DeployerService is IDeployerService { issuer.changeWalletApprover(request.issuerWalletApprover); issuer.changeOwnership(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); } @@ -325,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); } @@ -395,7 +396,7 @@ contract DeployerService is IDeployerService { issuer.changeWalletApprover(request.issuerWalletApprover); issuer.changeOwnership(request.issuerOwner); asset.changeOwnership(request.assetOwner); - campaign.changeOwnership(request.cfManagerOwner); + Ownable(address(campaign)).transferOwnership(request.cfManagerOwner); emit DeployIssuerAssetTransferableCampaign( msg.sender, @@ -453,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); } @@ -503,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); } From fcbc9aa7fa22d203d0d2979fb8a106bee46f575a Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Mon, 18 Jul 2022 12:34:50 +0200 Subject: [PATCH 08/10] make issuer ownable (by oz standards) --- contracts/issuer/IIssuer.sol | 4 ---- contracts/issuer/Issuer.sol | 20 ++++++-------------- contracts/services/DeployerService.sol | 4 ++-- 3 files changed, 8 insertions(+), 20 deletions(-) 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/services/DeployerService.sol b/contracts/services/DeployerService.sol index 324431e..d3c79db 100644 --- a/contracts/services/DeployerService.sol +++ b/contracts/services/DeployerService.sol @@ -271,7 +271,7 @@ 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); Ownable(address(campaign)).transferOwnership(request.cfManagerOwner); @@ -394,7 +394,7 @@ 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); Ownable(address(campaign)).transferOwnership(request.cfManagerOwner); From f62e9fe8420bacbd07c76bfca3f2ef68a08dba56 Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Thu, 8 Sep 2022 17:39:59 +0200 Subject: [PATCH 09/10] commit floating changes --- contracts/managers/rewarder/Rewarder.sol | 81 +++++ contracts/managers/salary-manager/Payroll.sol | 74 ++++ .../SupplyChainManager.sol | 343 ++++++++++++++++++ test/managers/rewarder-test.ts | 112 ++++++ test/managers/supply-chain-manager-test.ts | 265 ++++++++++++++ 5 files changed, 875 insertions(+) create mode 100644 contracts/managers/rewarder/Rewarder.sol create mode 100644 contracts/managers/salary-manager/Payroll.sol create mode 100644 contracts/managers/supply-chain-manager/SupplyChainManager.sol create mode 100644 test/managers/rewarder-test.ts create mode 100644 test/managers/supply-chain-manager-test.ts 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/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) + ); + }); + +}); From 76d6b0a76c7cb287cae3583ed9901819d5335206 Mon Sep 17 00:00:00 2001 From: Filip Dujmusic Date: Mon, 26 Sep 2022 12:57:41 +0200 Subject: [PATCH 10/10] add mintable nft and nft rewarder contracts --- .../nft-basic-mintable/NftBasicMintable.sol | 28 +++++++++ .../managers/nft-rewarder/NftRewarder.sol | 57 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 contracts/managers/nft-basic-mintable/NftBasicMintable.sol create mode 100644 contracts/managers/nft-rewarder/NftRewarder.sol 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); + } +}