Skip to content

Commit

Permalink
add tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
malteish committed Sep 27, 2023
1 parent 36d0a9f commit 5d3075a
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 10 deletions.
33 changes: 24 additions & 9 deletions docs/fees.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,40 @@ Investor buys X tokens for Y USDC through the ContinuousFundraising contract.

### Fee limits

Maximum fee is 5%, minimum fee is 0.
The minimum fee is 0.

Maximum fees are:

- 5% of tokens minted
- 5% of currency paid when using PersonalInvite
- 10% of currency paid when using ContinuousFundraising

## Fee collectors

The three fee types can be collected by different addresses, the fee collectors. The fee collectors are set by tokenize.it and can be changed by tokenize.it.

### Splitting fees

In case the fees collected must be split between multiple parties, one or more fee collector addresses can be set to one or more [PaymentSplitter](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.9/contracts/finance/PaymentSplitter.sol) contracts. This contract will then receive the fees and send each beneficiary their share. The payout can be triggered by anyone.

There is a limitation we are very unlikely to ever experience: if `totalAmountOfTokensReceived * highestShareNumber` overflows, the contract will not release these funds anymore. As we are using it for fees, which are just a small fraction of the total amount of tokens or currency, we should not see this happening in practice. Choosing the number of shares as low as possible is recommended to be even safer. See `testLockingFunds` in [the PaymentSplitter tests](./test/PaymentSplitter.t.sol) for a demonstration of this limitation.

The PaymentSplitter contract will be removed in Openzeppelin contracts 5.0, but an updated version should be introduced in a later version. Until then, the 4.9.x version can be used. See [this PR](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4276).

## Implementation

### Fee settings

Tokenize.it will deploy and manage at least one [fee settings contract](../contracts/FeeSettings.sol). This contract implements the IFeeSettingsV1 interface and thus can be queried for:
Tokenize.it will deploy and manage at least one [fee settings contract](../contracts/FeeSettings.sol). This contract implements the IFeeSettingsV2 interface and thus can be queried for:

- fee calculation:
- `tokenFee(uint256 tokenBuyAmount)`
- `continuousFundraisingFee(uint256 paymentAmount)`
- `personalInviteFee(uint256 paymentAmount)`
- feeCollector address
- feeCollector addresses
- `tokenFeeCollector()`
- `continuousFundraisingFeeCollector()`
- `personalInviteFeeCollector()`

These values can be changed by tokenize.it. Fee changes are subject to a delay of at least 12 weeks.

Expand All @@ -51,12 +72,6 @@ All fees are calculated as follows:
fee = amount / feeDenominator
```

Taking into account the [limits](#fee-limits), the following is enforced for all denominators:

```solidity
denominator >= 20
```

### Token contracts

- Each [token contract](../contracts/Token.sol) is connected to a [fee settings contract](../contracts/FeeSettings.sol).
Expand Down
106 changes: 105 additions & 1 deletion test/PaymentSplitter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,33 @@ contract paymentSplitterTest is Test {
FakePaymentToken token;

function setUp() public {
token = new FakePaymentToken(1000e18, 18);
token = new FakePaymentToken(type(uint256).max, 18);
}

function testVariableSplit(uint8 shares0, uint8 shares1, uint128 amount) public {
vm.assume(shares0 > 0);
vm.assume(shares1 > 0);
vm.assume(amount > uint128(shares0) + shares1);
address[] memory payees = new address[](2);
payees[0] = address(0x1);
payees[1] = address(0x2);
uint256[] memory shares = new uint256[](2);
shares[0] = shares0; // 80%
shares[1] = shares1; // 20%
PaymentSplitter splitter = new PaymentSplitter(payees, shares);

// send amount tokens to the splitter
token.transfer(address(splitter), amount);

// pull share for address 1
assertEq(token.balanceOf(payees[0]), 0);
splitter.release(token, payees[0]);
assertEq(token.balanceOf(payees[0]), (uint256(amount) * shares0) / (uint256(shares0) + shares1));

// pull share for address 2
assertEq(token.balanceOf(payees[1]), 0);
splitter.release(token, payees[1]);
assertEq(token.balanceOf(payees[1]), (uint256(amount) * shares1) / (uint256(shares0) + shares1));
}

function testFixedSplit() public {
Expand All @@ -34,4 +60,82 @@ contract paymentSplitterTest is Test {
splitter.release(token, payees[1]);
assertEq(token.balanceOf(payees[1]), 20e18);
}

function testMultiplePayments() public {
address[] memory payees = new address[](2);
payees[0] = address(0x1);
payees[1] = address(0x2);
uint256[] memory shares = new uint256[](2);
shares[0] = 8; // 80%
shares[1] = 2; // 20%
PaymentSplitter splitter = new PaymentSplitter(payees, shares);

// send 100 tokens to the splitter
token.transfer(address(splitter), 100e18);

// pull share for address 1
assertEq(token.balanceOf(payees[0]), 0);
splitter.release(token, payees[0]);
assertEq(token.balanceOf(payees[0]), 80e18);

// send another 300 tokens to the splitter
token.transfer(address(splitter), 300e18);

// pull full share for address 2
assertEq(token.balanceOf(payees[1]), 0);
splitter.release(token, payees[1]);
assertEq(token.balanceOf(payees[1]), (((100 + 300) * 20) / 100) * 1e18);

// pull remaining share for address 1
splitter.release(token, payees[0]);
assertEq(token.balanceOf(payees[0]), (((100 + 300) * 80) / 100) * 1e18);
}

function testAnyoneCanTriggerPayout(address rando) public {
address[] memory payees = new address[](2);
payees[0] = address(0x1);
payees[1] = address(0x2);
uint256[] memory shares = new uint256[](2);
shares[0] = 1;
shares[1] = 1;
PaymentSplitter splitter = new PaymentSplitter(payees, shares);

// send 100 tokens to the splitter
token.transfer(address(splitter), 100e18);

// pull share for address 1
assertEq(token.balanceOf(payees[0]), 0);
vm.prank(rando);
splitter.release(token, payees[0]);
assertEq(token.balanceOf(payees[0]), 50e18);
}

function testLockingFunds() public {
uint256 shares0 = 100e6;
uint256 shares1 = 100e6;
uint256 amount = type(uint256).max / 1e6;

// note how amount * shares0 > type(uint128).max

address[] memory payees = new address[](2);
payees[0] = address(0x1);
payees[1] = address(0x2);
uint256[] memory shares = new uint256[](2);
shares[0] = shares0; // 80%
shares[1] = shares1; // 20%
PaymentSplitter splitter = new PaymentSplitter(payees, shares);

// send amount tokens to the splitter
token.transfer(address(splitter), amount);

assertEq(token.balanceOf(payees[0]), 0);

// try pulling share for address 1
// this will fail because it overflows during the calculation inside PaymentSplitter
vm.expectRevert();
splitter.release(token, payees[0]);

// should be fine for our use case though: the sum of all fees will always be significantly less than the total supply,
// and we will choose a sum of shares of less than 1000.
}
}

0 comments on commit 5d3075a

Please sign in to comment.