Skip to content

Commit

Permalink
Merge pull request #222 from corpus-io/feature/transferAndCall
Browse files Browse the repository at this point in the history
Feature/transfer and call
  • Loading branch information
malteish authored Nov 8, 2023
2 parents 2b825b8 + 31af78e commit bbdaf6f
Show file tree
Hide file tree
Showing 6 changed files with 654 additions and 16 deletions.
74 changes: 65 additions & 9 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,82 @@ 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 {
// 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);
_checkAndDeliver(_amount, _tokenReceiver);

token.mint(_tokenReceiver, _amount);
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) {
return Math.ceilDiv(_tokenAmount * getPrice(), 10 ** token.decimals());
}

function findMaxAmount(uint256 _minAmount) external view returns (uint256) {
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);

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

_checkAndDeliver(amount, tokenReceiver);

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
93 changes: 86 additions & 7 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 @@ -401,9 +396,9 @@ contract PublicFundraisingTest is Test {
uint256 availableBalance = paymentToken.balanceOf(buyer);

vm.prank(buyer);
paymentToken.transfer(person1, availableBalance / 2);
paymentToken.transfer(person1, availableBalance / 3);
vm.prank(buyer);
paymentToken.transfer(person2, 10 ** 6);
paymentToken.transfer(person2, availableBalance / 3);

vm.prank(person1);
paymentToken.approve(address(raise), paymentTokenAmount);
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

0 comments on commit bbdaf6f

Please sign in to comment.