Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/transfer and call #222

Merged
merged 11 commits into from
Nov 8, 2023
75 changes: 65 additions & 10 deletions contracts/PublicFundraising.sol
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,7 @@ contract PublicFundraising is
return priceBase;
}

/**
* @notice Buy `amount` tokens and mint them to `_tokenReceiver`.
* @param _amount amount of tokens to buy, in bits (smallest subunit of token)
* @param _tokenReceiver address the tokens should be minted to
*/
function buy(uint256 _amount, address _tokenReceiver) external whenNotPaused nonReentrant {
function _checkAndDeliver(uint256 _amount, address _tokenReceiver) internal {
require(tokensSold + _amount <= maxAmountOfTokenToBeSold, "Not enough tokens to sell left");
require(tokensBought[_tokenReceiver] + _amount >= minAmountPerBuyer, "Buyer needs to buy at least minAmount");
require(
Expand All @@ -187,21 +182,81 @@ contract PublicFundraising is
tokensSold += _amount;
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 * getPrice(), 10 ** token.decimals());
token.mint(_tokenReceiver, _amount);
}

function _getFeeAndFeeReceiver(uint256 _currencyAmount) internal view returns (uint256, address) {
IFeeSettingsV2 feeSettings = token.feeSettings();
return (feeSettings.publicFundraisingFee(_currencyAmount), feeSettings.publicFundraisingFeeCollector());
}

/**
* @notice Buy `amount` tokens and mint them to `_tokenReceiver`.
* @param _amount amount of tokens to buy, in bits (smallest subunit of token)
* @param _tokenReceiver address the tokens should be minted to
*/
function buy(uint256 _amount, address _tokenReceiver) public whenNotPaused nonReentrant {
_checkAndDeliver(_amount, _tokenReceiver);
// 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 = calculateCurrencyAmountFromTokenAmount(_amount);
IFeeSettingsV2 feeSettings = token.feeSettings();
uint256 fee = feeSettings.publicFundraisingFee(currencyAmount);
if (fee != 0) {
currency.safeTransferFrom(_msgSender(), feeSettings.publicFundraisingFeeCollector(), fee);
}

currency.safeTransferFrom(_msgSender(), currencyReceiver, currencyAmount - fee);

token.mint(_tokenReceiver, _amount);
malteish marked this conversation as resolved.
Show resolved Hide resolved
emit TokensBought(_msgSender(), _amount, currencyAmount);
}

/// calculate token amount from currency amount and price. Must be rounded down anyway, so the normal integer math is fine.
/// This calculation often results in a larger amount of tokens
function calculateTokenAmountFromCurrencyAmount(uint256 _currencyAmount) public view returns (uint256) {
return (_currencyAmount * 10 ** token.decimals()) / getPrice();
}

function calculateCurrencyAmountFromTokenAmount(uint256 _tokenAmount) public view returns (uint256) {
malteish marked this conversation as resolved.
Show resolved Hide resolved
return Math.ceilDiv(_tokenAmount * getPrice(), 10 ** token.decimals());
}

function findMaxAmount(uint256 _minAmount) external view returns (uint256) {
malteish marked this conversation as resolved.
Show resolved Hide resolved
uint256 currencyAmount = calculateCurrencyAmountFromTokenAmount(_minAmount);
return (calculateTokenAmountFromCurrencyAmount(currencyAmount));
}

function onTokenTransfer(
address _from,
uint256 _currencyAmount,
bytes calldata data
) external whenNotPaused nonReentrant returns (bool) {
require(_msgSender() == address(currency), "only the currency contract can call this function");

// if a recipient address was provided in data, use it as receiver. Otherwise, use _from as receiver.
address tokenReceiver;
if (data.length == 32) {
tokenReceiver = abi.decode(data, (address));
} else {
tokenReceiver = _from;
}

// address tokenReceiver = abi.decode(data, (address));
// tokenReceiver = tokenReceiver == address(0) ? _from : tokenReceiver;

uint256 amount = calculateTokenAmountFromCurrencyAmount(_currencyAmount);

_checkAndDeliver(amount, tokenReceiver);

// move payment to currencyReceiver and feeCollector
(uint256 fee, address feeCollector) = _getFeeAndFeeReceiver(_currencyAmount);
currency.safeTransfer(feeCollector, fee);
currency.safeTransfer(currencyReceiver, _currencyAmount - fee);

emit TokensBought(_from, amount, _currencyAmount);

// return true is an antipattern, but required by the interface
return true;
}

/**
* @notice change the currencyReceiver to `_currencyReceiver`
* @param _currencyReceiver new currencyReceiver
Expand Down
89 changes: 84 additions & 5 deletions test/PublicFundraising.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,6 @@ contract PublicFundraisingTest is Test {
}

function testBuyTooMuch() public {
uint256 tokenBuyAmount = 5 * 10 ** token.decimals();
uint256 costInPaymentToken = (tokenBuyAmount * price) / 10 ** 18;

assert(costInPaymentToken == 35 * 10 ** paymentTokenDecimals); // 35 payment tokens, manually calculated

uint256 paymentTokenBalanceBefore = paymentToken.balanceOf(buyer);

vm.prank(buyer);
Expand Down Expand Up @@ -1212,4 +1207,88 @@ contract PublicFundraisingTest is Test {
raise.acceptOwnership();
assertTrue(raise.owner() == newOwner);
}

function testMaxAmountFixed() public {
uint256 _price = 7 * 10 ** paymentTokenDecimals; // 7 payment tokens per token
PublicFundraising _raise = PublicFundraising(
factory.createPublicFundraisingClone(
bytes32("a"),
trustedForwarder,
owner,
payable(receiver),
minAmountPerBuyer,
maxAmountPerBuyer,
_price,
maxAmountOfTokenToBeSold,
paymentToken,
token
)
);

// If I want to buy 1 tokens bit, I need to pay 1 payment token bit, even though
// the "real" cost would only be 7/(10^12) payment tokens
// Therefore, it is cleverer if I buy as much as possible for that 1 payment token bit, which is 1/7 tokens
uint256 _amount = 1; // token bit
uint256 _currencyAmount = _raise.calculateCurrencyAmountFromTokenAmount(_amount); // 1 payment token bit
uint256 _maxAmountManual = _raise.calculateTokenAmountFromCurrencyAmount(_currencyAmount); // 1/7 * 10^12 tokens
uint256 _maxAmount = _raise.findMaxAmount(_amount);
uint256 _effectivePrice = (_currencyAmount * 10 ** token.decimals()) / _maxAmount;

// log all 3 values
console.log("amount", _amount);
console.log("currencyAmount", _currencyAmount);
console.log("maxAmount", _maxAmount);
// difference between amount and maxAmount
console.log("difference", _maxAmount - _amount);
// price calculated from _maxAmount and _currencyAmount
console.log("price", (_currencyAmount * 10 ** token.decimals()) / _maxAmount);

assertTrue(_effectivePrice == _price, "Prices don't match");
assertTrue(_maxAmount == uint256(10 ** 12) / 7, "Max amount is wrong");
assertTrue(_maxAmount == _maxAmountManual, "Max amounts don't match");
}

function testMaxAmountVariable(uint256 _price, uint256 _amount) public {
vm.assume(_price > 0);
vm.assume(_amount > 0);
vm.assume(_amount < type(uint256).max / _price);

PublicFundraising _raise = PublicFundraising(
factory.createPublicFundraisingClone(
bytes32("a"),
trustedForwarder,
owner,
payable(receiver),
minAmountPerBuyer,
maxAmountPerBuyer,
_price,
maxAmountOfTokenToBeSold,
paymentToken,
token
)
);

uint256 _currencyAmount = _raise.calculateCurrencyAmountFromTokenAmount(_amount);

vm.assume(_currencyAmount < type(uint256).max / 10 ** token.decimals()); // otherwise an overflow will occur
uint256 _maxAmountManual = _raise.calculateTokenAmountFromCurrencyAmount(_currencyAmount);
uint256 _maxAmount = _raise.findMaxAmount(_amount);
uint256 _effectivePriceForMax = (_currencyAmount * 10 ** token.decimals()) / _maxAmount;
uint256 _effectivePriceForInput = (_currencyAmount * 10 ** token.decimals()) / _amount;

// log all 3 values
console.log("amount", _amount);
console.log("currencyAmount", _currencyAmount);
console.log("maxAmount", _maxAmount);
// difference between amount and maxAmount
console.log("difference", _maxAmount - _amount);
// price calculated from _maxAmount and _currencyAmount
console.log("_effectivePriceForMax", _effectivePriceForMax);
console.log("price", _price);

assertTrue(_effectivePriceForInput >= _price, "Effective price lower than nominal price!");
assertTrue(_effectivePriceForMax >= _price, "Effective price for max amount lower than nominal price");
assertTrue(_maxAmount >= _amount, "Max amount is wrong");
assertTrue(_maxAmount == _maxAmountManual, "Max amounts don't match");
}
}
Loading
Loading