Skip to content

Commit

Permalink
Merge pull request #210 from corpus-io/feature/dynamicPrice
Browse files Browse the repository at this point in the history
Feature/dynamic price
  • Loading branch information
malteish authored Nov 8, 2023
2 parents 1e62e76 + 8ff2f7e commit 2b825b8
Show file tree
Hide file tree
Showing 8 changed files with 767 additions and 23 deletions.
154 changes: 154 additions & 0 deletions contracts/PriceLinear.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.17;

import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "./interfaces/IPriceDynamic.sol";

struct Linear {
/// numerator of slope of linear function, e.g. a where slope == a/b
uint64 slopeEnumerator;
/// denominator of slope of linear function, , e.g. b where slope == a/b
uint64 slopeDenominator;
/// start time as unix timestamp or start block number
uint64 start;
/// step width in seconds or blocks
uint32 stepDuration;
/// if 0: `stepDuration` is in seconds and `start` is an epoch
/// if 1: `stepDuration` is in blocks and `start` is a block number
bool isBlockBased;
/// if 0 price is falling, if 1 price is rising
bool isRising;
}

/**
* @title Linear price function, with option for stepping, based on time or block count
* @author malteish
* @notice This contract implements a linear
* @dev The contract inherits from ERC2771Context in order to be usable with Gas Station Network (GSN) https://docs.opengsn.org/faq/troubleshooting.html#my-contract-is-using-openzeppelin-how-do-i-add-gsn-support
*/
contract PriceLinear is ERC2771ContextUpgradeable, Ownable2StepUpgradeable, IPriceDynamic {
Linear public parameters;

/**
* This constructor creates a logic contract that is used to clone new fundraising contracts.
* It has no owner, and can not be used directly.
* @param _trustedForwarder This address can execute transactions in the name of any other address
*/
constructor(address _trustedForwarder) ERC2771ContextUpgradeable(_trustedForwarder) {
_disableInitializers();
}

/**
* @notice Sets up the PublicFundraising. The contract is usable immediately after deployment, but does need a minting allowance for the token.
* @dev Constructor that passes the trusted forwarder to the ERC2771Context constructor
*/
function initialize(
address _owner,
uint64 _slopeEnumerator,
uint64 _slopeDenominator,
uint64 _startTimeOrBlockNumber,
uint32 _stepDuration,
bool _isBlockBased,
bool _isRising
) external initializer {
require(_owner != address(0), "owner can not be zero address");
__Ownable2Step_init(); // sets msgSender() as owner
_transferOwnership(_owner); // sets owner as owner
_updateParameters(
_slopeEnumerator,
_slopeDenominator,
_startTimeOrBlockNumber,
_stepDuration,
_isBlockBased,
_isRising
);
}

/**
* Update the parameters of the linear price function
*/
function updateParameters(
uint64 _slopeEnumerator,
uint64 _slopeDenominator,
uint64 _startTimeOrBlockNumber,
uint32 _stepDuration,
bool _isBlockBased,
bool _isRising
) external onlyOwner {
_updateParameters(
_slopeEnumerator,
_slopeDenominator,
_startTimeOrBlockNumber,
_stepDuration,
_isBlockBased,
_isRising
);
}

function _updateParameters(
uint64 _slopeEnumerator,
uint64 _slopeDenominator,
uint64 _startTimeOrBlockNumber,
uint32 _stepDuration,
bool _isBlockBased,
bool _isRising
) internal {
require(_slopeEnumerator != 0, "slopeEnumerator can not be zero");
require(_slopeDenominator != 0, "slopeDenominator can not be zero");
require(_startTimeOrBlockNumber > block.timestamp, "startTime must be in the future");
require(_stepDuration != 0, "stepDuration can not be zero");
parameters = Linear({
slopeEnumerator: _slopeEnumerator,
slopeDenominator: _slopeDenominator,
start: _startTimeOrBlockNumber,
stepDuration: _stepDuration,
isBlockBased: _isBlockBased,
isRising: _isRising
});
}

function getPrice(uint256 basePrice) public view returns (uint256) {
Linear memory _parameters = parameters;
uint256 current = _parameters.isBlockBased ? block.number : block.timestamp;

if (current <= _parameters.start) {
return basePrice;
}

/// @dev note that the division is rounded down, generating a step function if stepDuration > 1
uint256 change = (((current - _parameters.start) / _parameters.stepDuration) *
_parameters.stepDuration *
_parameters.slopeEnumerator) / _parameters.slopeDenominator;

//uint256 change = uint256((current - parameters.start) / parameters.stepDuration) * parameters.slopeEnumerator;

// if price is rising, add change, else subtract change
if (_parameters.isRising) {
// prevent overflow
if (type(uint256).max - basePrice <= change) {
return type(uint256).max;
}
return basePrice + change;
} else {
// prevent underflow
return basePrice <= change ? 0 : basePrice - change;
}
}

/**
* @dev both Ownable and ERC2771Context have a _msgSender() function, so we need to override and select which one to use.
*/
function _msgSender() internal view override(ContextUpgradeable, ERC2771ContextUpgradeable) returns (address) {
return ERC2771ContextUpgradeable._msgSender();
}

/**
* @dev both Ownable and ERC2771Context have a _msgData() function, so we need to override and select which one to use.
*/
function _msgData() internal view override(ContextUpgradeable, ERC2771ContextUpgradeable) returns (bytes calldata) {
return ERC2771ContextUpgradeable._msgData();
}
}
104 changes: 104 additions & 0 deletions contracts/PriceLinearCloneFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.17;

import "./PriceLinear.sol";
import "./CloneFactory.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";

contract PriceLinearCloneFactory is CloneFactory {
constructor(address _implementation) CloneFactory(_implementation) {}

function createPriceLinear(
bytes32 _rawSalt,
address _trustedForwarder,
address _owner,
uint64 _slopeEnumerator,
uint64 _slopeDenominator,
uint64 _startTimeOrBlockNumber,
uint32 _stepDuration,
bool _isBlockBased,
bool _isRising
) external returns (address) {
bytes32 salt = _generateSalt(
_rawSalt,
_trustedForwarder,
_owner,
_slopeEnumerator,
_slopeDenominator,
_startTimeOrBlockNumber,
_stepDuration,
_isBlockBased,
_isRising
);
address clone = Clones.cloneDeterministic(implementation, salt);
PriceLinear clonePriceOracle = PriceLinear(clone);
require(
clonePriceOracle.isTrustedForwarder(_trustedForwarder),
"PriceLinearCloneFactory: Unexpected trustedForwarder"
);
clonePriceOracle.initialize(
_owner,
_slopeEnumerator,
_slopeDenominator,
_startTimeOrBlockNumber,
_stepDuration,
_isBlockBased,
_isRising
);
emit NewClone(clone);
return clone;
}

function predictCloneAddress(
bytes32 _rawSalt,
address _trustedForwarder,
address _owner,
uint64 _slopeEnumerator,
uint64 _slopeDenominator,
uint64 _startTimeOrBlockNumber,
uint32 _stepDuration,
bool _isBlockBased,
bool _isRising
) external view returns (address) {
bytes32 salt = _generateSalt(
_rawSalt,
_trustedForwarder,
_owner,
_slopeEnumerator,
_slopeDenominator,
_startTimeOrBlockNumber,
_stepDuration,
_isBlockBased,
_isRising
);
return Clones.predictDeterministicAddress(implementation, salt);
}

function _generateSalt(
bytes32 _rawSalt,
address _trustedForwarder,
address _owner,
uint64 _slopeEnumerator,
uint64 _slopeDenominator,
uint64 _startTimeOrBlockNumber,
uint32 _stepDuration,
bool _isBlockBased,
bool _isRising
) internal pure returns (bytes32) {
return
keccak256(
abi.encodePacked(
_rawSalt,
_trustedForwarder,
_owner,
_slopeEnumerator,
_slopeDenominator,
_startTimeOrBlockNumber,
_stepDuration,
_isBlockBased,
_isRising
)
);
}
}
44 changes: 40 additions & 4 deletions contracts/PublicFundraising.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

import "./Token.sol";
import "./interfaces/IPriceDynamic.sol";

/**
* @title PublicFundraising
Expand Down Expand Up @@ -38,7 +39,12 @@ contract PublicFundraising is
uint256 public maxAmountPerBuyer;
/// The price of a token, expressed as amount of bits of currency per main unit token (e.g.: 2 USDC (6 decimals) per TOK (18 decimals) => price = 2*10^6 ).
/// @dev units: [tokenPrice] = [currency_bits]/[token], so for above example: [tokenPrice] = [USDC_bits]/[TOK]
uint256 public tokenPrice;
uint256 public priceBase;
uint256 public priceMin;
uint256 public priceMax;
/// dynamic pricing oracle
IPriceDynamic public priceOracle;

/// total amount of tokens that CAN BE minted through this contract, in bits (bit = smallest subunit of token)
uint256 public maxAmountOfTokenToBeSold;
/// total amount of tokens that HAVE BEEN minted through this contract, in bits (bit = smallest subunit of token)
Expand Down Expand Up @@ -80,6 +86,8 @@ contract PublicFundraising is
*/
event TokensBought(address indexed buyer, uint256 tokenAmount, uint256 currencyAmount);

event DynamicPricingActivated(address priceOracle, uint256 priceMin, uint256 priceMax);

/**
* This constructor creates a logic contract that is used to clone new fundraising contracts.
* It has no owner, and can not be used directly.
Expand Down Expand Up @@ -118,7 +126,7 @@ contract PublicFundraising is
currencyReceiver = _currencyReceiver;
minAmountPerBuyer = _minAmountPerBuyer;
maxAmountPerBuyer = _maxAmountPerBuyer;
tokenPrice = _tokenPrice;
priceBase = _tokenPrice;
maxAmountOfTokenToBeSold = _maxAmountOfTokenToBeSold;
currency = _currency;
token = _token;
Expand All @@ -135,6 +143,34 @@ contract PublicFundraising is
// after creating the contract, it needs a minting allowance (in the token contract)
}

function activateDynamicPricing(
IPriceDynamic _priceOracle,
uint256 _priceMin,
uint256 _priceMax
) external onlyOwner whenPaused {
require(address(_priceOracle) != address(0), "_priceOracle can not be zero address");
priceOracle = _priceOracle;
require(_priceMin <= priceBase, "_priceMin needs to be smaller or equal to priceBase");
priceMin = _priceMin;
require(priceBase <= _priceMax, "_priceMax needs to be larger or equal to priceBase");
priceMax = _priceMax;
coolDownStart = block.timestamp;

emit DynamicPricingActivated(address(_priceOracle), _priceMin, _priceMax);
}

function deactivateDynamicPricing() external onlyOwner whenPaused {
priceOracle = IPriceDynamic(address(0));
}

function getPrice() public view returns (uint256) {
if (address(priceOracle) != address(0)) {
return Math.min(Math.max(priceOracle.getPrice(priceBase), priceMin), priceMax);
}

return priceBase;
}

/**
* @notice Buy `amount` tokens and mint them to `_tokenReceiver`.
* @param _amount amount of tokens to buy, in bits (smallest subunit of token)
Expand All @@ -152,7 +188,7 @@ contract PublicFundraising is
tokensBought[_tokenReceiver] += _amount;

// rounding up to the next whole number. Investor is charged up to one currency bit more in case of a fractional currency bit.
uint256 currencyAmount = Math.ceilDiv(_amount * tokenPrice, 10 ** token.decimals());
uint256 currencyAmount = Math.ceilDiv(_amount * getPrice(), 10 ** token.decimals());

IFeeSettingsV2 feeSettings = token.feeSettings();
uint256 fee = feeSettings.publicFundraisingFee(currencyAmount);
Expand Down Expand Up @@ -206,7 +242,7 @@ contract PublicFundraising is
*/
function setCurrencyAndTokenPrice(IERC20 _currency, uint256 _tokenPrice) external onlyOwner whenPaused {
require(_tokenPrice != 0, "_tokenPrice needs to be a non-zero amount");
tokenPrice = _tokenPrice;
priceBase = _tokenPrice;
currency = _currency;
emit TokenPriceAndCurrencyChanged(_tokenPrice, _currency);
coolDownStart = block.timestamp;
Expand Down
6 changes: 6 additions & 0 deletions contracts/interfaces/IPriceDynamic.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.17;

interface IPriceDynamic {
function getPrice(uint256 basePrice) external view returns (uint256);
}
Loading

0 comments on commit 2b825b8

Please sign in to comment.