Skip to content
Open
Show file tree
Hide file tree
Changes from 76 commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
30dc9c3
v0.1 of the distribution and co-investor feature - not done yet
CJentzsch Jan 9, 2026
fc37ce9
fix missing OnlyOwner
CJentzsch Jan 9, 2026
adb90e0
prettier
Feb 16, 2026
f2e1687
prettier and clarifications on Coinvestor
Feb 16, 2026
f1b1b75
vscode autoprettification settings
Feb 16, 2026
c635edf
comment on future gas optimization
Feb 16, 2026
8d46dcb
draft one contract for holding and swapping coinvested tokens
Feb 16, 2026
767f486
reduce code duplication 4 TokenSwap
Feb 16, 2026
edd0fe6
unify recipient handling
Feb 16, 2026
0ea6b47
improve storage structure
Feb 16, 2026
09300c3
improve tokenPrice handling
Feb 16, 2026
5ca9a0e
fix Distribution compile errors
Feb 23, 2026
4ae0d3c
fix carry distribution logic
Feb 23, 2026
3332d83
move distribution split logic to TokenSwapCoinvestor
Feb 23, 2026
308f04f
update name to reflect usage
Feb 23, 2026
e91b61e
explicitly disable initializer
Feb 23, 2026
6134fe9
remove unnecessary function
Feb 23, 2026
804b043
cleanup
Feb 23, 2026
27e85e1
prevent carry > 1
Feb 23, 2026
5c2c8fb
unify carry distribution
Feb 23, 2026
eb96714
check if currency is trusted
Feb 23, 2026
02e81cd
fail on currency mismatch
Feb 23, 2026
8b138d2
use SafeERC20 in Distribution to handle non-reverting tokens
Feb 24, 2026
746e5b6
allow Distribution reassignment
Feb 24, 2026
dddf4e2
timeout before reassignment
Feb 24, 2026
549e851
improve definitions
Feb 24, 2026
2f5e119
make Distribution cloneable
Feb 24, 2026
6479878
improve currency check
Feb 24, 2026
c28ad5a
add clone factory for CoinvestedPosition
Feb 24, 2026
d0326d9
update exit path
Feb 24, 2026
fb978df
reference base price to TRUSTED_EURO_CURRENCY
Feb 24, 2026
c7f43e3
BREAKING: change trusted currency check
Feb 24, 2026
14f5891
add currency change and streamline distribution logic
Feb 25, 2026
2c2f344
drain exit proceeds are claim window closes
Feb 25, 2026
f38301e
more flexible absolute exit claim period
Feb 25, 2026
2014fe6
prettier
Feb 25, 2026
b422689
fix factory
Feb 25, 2026
5ee4a4e
clarifications
Feb 25, 2026
6585ee3
update tests to reflect new handling of TRUSTED_CURRENCY
Feb 25, 2026
ea61abc
reject 0 carry fraction
Feb 26, 2026
1ea68e6
fix funding flow
Feb 26, 2026
c46fb82
update currency requirements
Feb 26, 2026
3037de4
plan testing suite
Feb 26, 2026
064b8f1
test implementation plan
Feb 26, 2026
280913e
introduce initializer arguments
Feb 26, 2026
1120ebb
collect fees during Exit and Distribution
Mar 3, 2026
029ce7f
remove unneccessary overload
Mar 3, 2026
2495656
remove flawed vesting claim and add option to fix manually
Mar 3, 2026
c2e0323
update carry enforcement, add first tests
Mar 5, 2026
61f3d01
add tests for CoinvestedPosition
Mar 6, 2026
ec10f43
test 2771 for CoinvestedPosition
Mar 6, 2026
69961a6
test CoinvestedPositionCloneFactory
Mar 6, 2026
6c6c6e5
add minCurrencyAmount guarding exit in CoinvestedPosition
Mar 6, 2026
c7367a8
allow 0 reassignment delay
Mar 7, 2026
0d6ea48
make sure snapshot is not 0
Mar 7, 2026
1893d29
remove duplicate state variable
Mar 7, 2026
fac81b8
add best practices guide
Mar 7, 2026
926f355
fix fee type
Mar 7, 2026
f327192
tests for Distribution
Mar 7, 2026
7093bb4
allow exit after deadline
Mar 9, 2026
029f378
tests for Exit
Mar 9, 2026
ad1b57a
add logic contract tests for CoinvestedPosition
Mar 9, 2026
f4bddb3
rename Exit.claimEnd to drainStart
Mar 10, 2026
165ed0f
tests for factories
Mar 10, 2026
5fbe5d8
prevent settling with base token
Mar 10, 2026
e41daa1
prevent distributing base token
Mar 10, 2026
68e954a
test for CoinvestedPosition - Distribution integration
Mar 10, 2026
6b72620
minor fixes
Mar 10, 2026
b4d43ca
add test demonstrating exit claim through safe
Mar 10, 2026
22a1da2
remove temp file
Mar 10, 2026
4d19ea2
fix fuzz test
Mar 10, 2026
b5d990b
exclude safe demo from default profile compilation
Mar 10, 2026
2e2d232
add docs
Mar 10, 2026
bea606b
amend dev docs
Mar 10, 2026
695e40f
update docs
Mar 10, 2026
87f24e5
fix fee docs
Mar 10, 2026
1d74d33
mention trust issue in exit
Mar 12, 2026
c7fbc32
remove ERC1271
Mar 12, 2026
384c4a5
update prettier config
Mar 12, 2026
c4fffc9
introduce dynamic fee types in IFeeSettingsV3
Mar 17, 2026
b40e885
fix spec
Mar 17, 2026
32845c5
fix tests
Mar 17, 2026
70a3031
prettier
Mar 17, 2026
e4cdf21
Revert "fix spec"
Mar 17, 2026
5d14237
rework initialization
Mar 17, 2026
3d48ec0
introduce dedicated Distribution and Exit fees
Mar 18, 2026
3fdc295
rework fee type strings
Mar 18, 2026
558575b
add view functions for easy auditability
Mar 18, 2026
ddfb618
Add feeTypeId function to convert fee type names to bytes32 keys for …
Mar 18, 2026
3472874
Revert "add view functions for easy auditability"
Mar 18, 2026
0aae8a5
Remove hardcoded maximum fee constants from FeeSettings
Mar 18, 2026
b4e7214
remove legacy Fees struct and update tests
Mar 18, 2026
cf18282
polish
Mar 18, 2026
37f9245
prettier
Mar 18, 2026
e689f07
polish
Mar 18, 2026
4724d9a
avoid fee type creation without collector
Mar 18, 2026
cc3aee5
test with legacy bytecode
Mar 19, 2026
3130a5b
add docs
Mar 19, 2026
bcc7b00
enable pipeline
Mar 19, 2026
90b2d18
prettier
Mar 19, 2026
bf4a12e
add minimal timelock
Mar 19, 2026
3ceb55b
use new timelock with private offers
Mar 19, 2026
20586fd
add Distribution reassignment at intialization
Mar 19, 2026
6f24606
cleanup
Mar 19, 2026
960d8d6
Merge pull request #403 from corpus-io/feat/newFeeSettings
malteish Mar 31, 2026
a52d931
Merge branch 'feat/TokenSwapCoinvestor' into feat/timelockRework
Mar 31, 2026
cc439ee
fix tests
Mar 31, 2026
2be3b91
remove manual currency declaration in claimDistribution
Mar 31, 2026
d148aa0
simplify distribution claim
Mar 31, 2026
700f88c
prettier
Mar 31, 2026
7483cfb
Merge pull request #405 from corpus-io/feat/timelockRework
malteish Mar 31, 2026
5ff0812
Merge pull request #404 from corpus-io/feat/testInterVersionWithBytecode
malteish Mar 31, 2026
c001787
revert the introduction of EURO_CURRENCY attribute
Mar 31, 2026
28255d1
ensure non-existing fee types calculate a zero fee
Mar 31, 2026
f30653f
update changelog
Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[solidity]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
}
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Changelog

## Unreleased

### New contract: `CoinvestedPosition`

Holds tokens on behalf of a co-investor and sells them, splitting proceeds between the co-investor (_receiver_) and lead investors via _carry_.

- **Base price:** EURO-denominated reference price recorded at initialization. After fees, the receiver is entitled to `basePrice` per token; any surplus is carry split among lead investors by `carryFraction`. If net proceeds don't cover `basePrice`, all proceeds go to the receiver.
- **Currency flexibility:** All three distribution paths (`buy`, dividends, exit) accept any EURO token (`TRUSTED_CURRENCY | EURO_CURRENCY`). The currency used by `buy()` is a state variable the owner can update via `setCurrency()`.
- **Balance sweep:** Lead investor shares are paid first, then the contract's entire remaining currency balance is swept to `receiver`, including any accidentally sent funds.

---

### New contract: `TokenSwapBase`

Abstract base extracted from duplicated logic in `TokenSwap` and `CoinvestedPosition`, covering shared state, fee handling, price/receiver management, pause controls, and ERC-2771 support. Both contracts now extend it.

---

### New contract: `Distribution`

Distributes a fixed currency amount among token holders proportional to their balance at a given snapshot. Supports direct claims, ERC-1271 smart-contract holders, and vesting contracts. An owner-only `reassign` function (available after a configurable delay post-deployment) handles recovery cases. Deployed via an atomic clone-and-fund factory.

## Fee collection

Note that for Distributions, all fees are collected at smart contract creation instead of at claim time. Rationale: there is no way to extract the currency without a fee payment, so we might as well collect at the beginning and save gas because we do it only once instead of bite-by-bite.

### New contract: `Exit`

Allows token holders to redeem tokens for a fixed currency payout within a configurable duration after the exit date. Deployed via an atomic clone-and-fund factory.

---

### New constant: `EURO_CURRENCY` in `AllowList`

`uint256 constant EURO_CURRENCY = 2 ** 254` (bit 254) added alongside `TRUSTED_CURRENCY` (bit 255). Marks a currency as Euro-denominated; required by `CoinvestedPosition` for all currency inputs.

---

### Breaking change: `TRUSTED_CURRENCY` check relaxed to bitmask

**Affected:** `Crowdinvesting`, `PrivateOffer`, `TokenSwap`

The currency allowList check changed from exact equality to a bitmask, allowing currency addresses to carry additional bits (e.g. `EURO_CURRENCY`) without being rejected. Existing deployments are affected when the attributes on the AllowList or the AllowList itself are updated.
23 changes: 13 additions & 10 deletions contracts/AllowList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol";

/**
* @dev the last bit is defined as special bit for the "trusted currency" attribute.
* This specific bit was chosen for this purpose because the allowList operators are
* unlikely to use it by chance in day to day operations. Contracts should check if this
* bit is set and all others are unset to ensure that the address is a trusted currency
* (hint: == 2**255).
* All other bits being zero means that the address has not proven any other relevant attributes
* to the allowList operator other than being a trusted currency, and thus the address is not
* able to receive tokens that require KYC or other attributes. This is intended behavior,
* as currency contracts receiving tokens is usually not intended.
* This constant is defined here so other contracts can easily access it.
* @dev The two highest bits are reserved for trusted currency classification:
* - TRUSTED_CURRENCY (bit 255): marks an address as a trusted ERC20 currency accepted for payments.
* - EURO_CURRENCY (bit 254): additionally marks the currency as Euro-denominated.
*
* Contracts should check for the relevant bit(s) using a bitmask, not equality, because
* currency addresses may have multiple currency-class bits set (e.g. TRUSTED_CURRENCY | EURO_CURRENCY).
* Example: to require a trusted EURO currency, check
* map[addr] & (TRUSTED_CURRENCY | EURO_CURRENCY) == (TRUSTED_CURRENCY | EURO_CURRENCY)
*
* Currency addresses intentionally carry no KYC or other user-attribute bits, so they cannot
* receive tokens that require such attributes. This is intended behavior, as currency contracts
* receiving tokens is usually not intended.
*/
uint256 constant TRUSTED_CURRENCY = 2 ** 255;
uint256 constant EURO_CURRENCY = 2 ** 254;

/**
* @title AllowList
Expand Down
245 changes: 245 additions & 0 deletions contracts/CoinvestedPosition.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.23;

import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

import "./TokenSwapBase.sol";
import "./IDistribution.sol";
import "./IExit.sol";

struct LeadInvestor {
/// lead investor address that receives carry
address account;
/// carry percentage, divided by uint64.max
uint64 carryFraction;
}

struct CoinvestedPositionInitializerArguments {
/// Owner of the contract
address owner;
/// coinvestor address: receives base price payout and carry dust
address receiver;
/// lead investors and their carry fractions
LeadInvestor[] leadInvestors;
/// base price per token in bits in currency below
uint256 basePrice;
/// currency used for buy() payments. Must be a EURO ERC20 (TRUSTED_CURRENCY | EURO_CURRENCY bits set on the token's allowList).
IERC20 baseCurrency;
/// token being held
Token token;
}

/**
* @title CoinvestedPosition
* @author malteish, cjentzsch
* @notice This contract holds tokens and sells them at a preset price, distributing proceeds
* between a coinvestor (receiver) and lead investors.
* The coinvestor (receiver) receives basePrice (a EURO reference price) per token sold.
* Any remaining proceeds after fees and coinvestor payout are split among lead investors
* according to their carry percentages, with dust going to the coinvestor.
* If the sale price minus fees is less than the base price, all proceeds go to the coinvestor.
* For exits, any EURO token (TRUSTED_CURRENCY | EURO_CURRENCY bits) may be used.
* For dividends, any trusted token (TRUSTED_CURRENCY bit) may be used.
* Neither needs to match the currency stored for buy().
* @dev Uses clone/proxy pattern. Constructor disables initializers, separate initialize().
*/
contract CoinvestedPosition is TokenSwapBase {
using SafeERC20 for IERC20;

/// lead investors and their carry fractions
LeadInvestor[] public leadInvestors;
/// base price per token in EURO bits (smallest subunit of any EURO currency)
uint256 public basePrice;
/// decimals of the currency used when basePrice was set; used to scale payouts when a different EURO token is used at exit/dividend time
uint8 public basePriceDecimals;

/**
* This constructor creates a logic contract that is used to clone new 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) TokenSwapBase(_trustedForwarder) {
_disableInitializers();
}

/**
* @notice Sets up the CoinvestedPosition. The contract is usable immediately after being initialized.
* @param _arguments Struct containing all arguments for the initializer
*/
function initialize(CoinvestedPositionInitializerArguments memory _arguments) external initializer {
_initializeBase(_arguments.owner, 0, _arguments.baseCurrency, _arguments.token, _arguments.receiver);

require(
_arguments.token.allowList().map(address(_arguments.baseCurrency)) & (TRUSTED_CURRENCY | EURO_CURRENCY) ==
(TRUSTED_CURRENCY | EURO_CURRENCY),
"currency must be a trusted EURO currency"
);
require(_arguments.leadInvestors.length > 0, "There must be at least one lead investor");
uint64 carryFractionsSum = 0;
for (uint256 i = 0; i < _arguments.leadInvestors.length; i++) {
require(_arguments.leadInvestors[i].account != address(0), "lead investor can not be zero address");
require(_arguments.leadInvestors[i].carryFraction > 0, "lead investor carry fraction can not be zero");
carryFractionsSum += _arguments.leadInvestors[i].carryFraction; // reverts on overflow
leadInvestors.push(_arguments.leadInvestors[i]);
}
basePrice = _arguments.basePrice;
basePriceDecimals = IERC20Metadata(address(_arguments.baseCurrency)).decimals();

// Pausing the contract prevents an immediate sell of the tokens. Once they should be sold, update price and unpause.
_pause();
}

/**
* @notice Change the payment currency to any trusted EURO currency.
* @dev basePrice remains in its original canonical units (basePriceDecimals); buy() scales it
* dynamically, so no re-scaling of basePrice is needed here.
* @param _currency new currency; must have TRUSTED_CURRENCY | EURO_CURRENCY bits set on the token's allowList
*/
function setCurrency(IERC20 _currency) external onlyOwner {
require(
token.allowList().map(address(_currency)) & (TRUSTED_CURRENCY | EURO_CURRENCY) ==
(TRUSTED_CURRENCY | EURO_CURRENCY),
"currency must be a trusted EURO currency"
);
currency = _currency;
}

/**
* @notice Buy `_tokenAmount` tokens and transfer them to `_tokenReceiver`.
* @param _tokenAmount amount of tokens to buy, in bits (smallest subunit of token)
* @param _maxCurrencyAmount maximum amount of currency to spend, in bits (smallest subunit of currency)
* @param _tokenReceiver address the tokens should be transferred to
*/
function buy(
uint256 _tokenAmount,
uint256 _maxCurrencyAmount,
address _tokenReceiver
) public whenNotPaused nonReentrant {
// rounding up to the next whole number. Buyer is charged up to one currency bit more in case of a fractional currency bit.
uint256 currencyAmount = Math.ceilDiv(_tokenAmount * tokenPrice, 10 ** token.decimals());

require(currencyAmount <= _maxCurrencyAmount, "Purchase more expensive than _maxCurrencyAmount");

// pull full amount to this contract first, then distribute from here
currency.safeTransferFrom(_msgSender(), address(this), currencyAmount);

// collect fee
(uint256 fee, address feeCollector) = _getFeeAndFeeReceiver(currencyAmount);
if (fee != 0) {
currency.safeTransfer(feeCollector, fee);
}

uint256 remaining = currencyAmount - fee;

// calculate carry: surplus above base price. If remaining <= base price, carry is 0 and receiver gets everything.
uint256 scaledBasePrice = _scaleToDecimals(basePrice, IERC20Metadata(address(currency)).decimals());
uint256 payoutCoinvestor = (scaledBasePrice * _tokenAmount) / (10 ** token.decimals());
uint256 carry = payoutCoinvestor < remaining ? remaining - payoutCoinvestor : 0;

_settle(carry, currency);

// transfer tokens from this contract to the buyer's receiver
token.transfer(_tokenReceiver, _tokenAmount);

emit TokensBought(_msgSender(), _tokenAmount, currencyAmount);
}

/**
* @notice Scales `_amount` from `basePriceDecimals` to `_targetDecimals`.
* @param _amount amount expressed in basePriceDecimals units
* @param _targetDecimals decimals of the target currency
* @return scaled amount in target currency units
*/
function _scaleToDecimals(uint256 _amount, uint8 _targetDecimals) internal view returns (uint256) {
if (_targetDecimals > basePriceDecimals) {
return _amount * 10 ** (_targetDecimals - basePriceDecimals);
} else if (_targetDecimals < basePriceDecimals) {
return _amount / 10 ** (basePriceDecimals - _targetDecimals);
}
return _amount;
}

/**
* @notice Distributes `carry` among lead investors by carryFraction, then sweeps the contract's
* full remaining balance of `_currency` to receiver. This even includes currency accidentally
* sent to the contract.
* @dev The sweep covers the base price portion and any rounding dust. Pass carry=0 when there is
* no surplus above base price; the loop produces no transfers and the full balance goes to receiver.
* @param carry surplus above base price to split among lead investors
* @param _currency the EURO token to settle
*/
function _settle(uint256 carry, IERC20 _currency) internal {
require(address(_currency) != address(token), "currency cannot be the held token");
for (uint256 i = 0; i < leadInvestors.length; i++) {
uint256 share = (uint256(leadInvestors[i].carryFraction) * carry) / type(uint64).max;
if (share != 0) {
_currency.safeTransfer(leadInvestors[i].account, share);
}
}
_currency.safeTransfer(receiver, _currency.balanceOf(address(this)));
}

/**
* @notice Claim this contract's eligible dividend share from `_dist` and split it among lead investors.
* @dev The full received amount is treated as carry and split among lead investors by carryFraction;
* remainder goes to receiver. Any trusted currency may be used (TRUSTED_CURRENCY bit required).
* @param _dist the Distribution (dividend) contract to claim from
* @param _dividendCurrency the currency paid out by the distribution
*/
function distributeDividends(IDistribution _dist, IERC20 _dividendCurrency) external onlyOwner nonReentrant {
require(
token.allowList().map(address(_dividendCurrency)) & TRUSTED_CURRENCY == TRUSTED_CURRENCY,
"dividend currency must be a trusted currency"
);
uint256 before = _dividendCurrency.balanceOf(address(this));
_dist.claim(address(this));
uint256 received = _dividendCurrency.balanceOf(address(this)) - before;
require(received > 0, "didn't receive expected currency from distribution");
_settle(received, _dividendCurrency);
}

/**
* @notice Claim exit proceeds for this contract's full token balance and split them among the receiver and lead investors.
* @dev Transfers all held tokens to the Exit contract in exchange for currency.
* If proceeds < base, receiver gets everything.
* Carry is split among lead investors by carryFraction; remainder goes to receiver.
* Any EURO token (TRUSTED_CURRENCY | EURO_CURRENCY) may be used, independent of the currency used for buy().
* @param _exit the Exit contract to claim from
* @param _exitCurrency the EURO token paid out by the exit
* @param _minCurrencyAmount minimum currency the call must receive; reverts if proceeds fall short.
* This guards against faulty or malicious exit contracts.
*/
function distributeExit(
IExit _exit,
IERC20 _exitCurrency,
uint256 _minCurrencyAmount
) external onlyOwner nonReentrant {
require(
token.allowList().map(address(_exitCurrency)) & (TRUSTED_CURRENCY | EURO_CURRENCY) ==
(TRUSTED_CURRENCY | EURO_CURRENCY),
"exit currency must be a trusted EURO currency"
);
uint256 tokenBalance = token.balanceOf(address(this));
require(tokenBalance > 0, "no tokens to claim");
IERC20(address(token)).approve(address(_exit), tokenBalance);
uint256 before = _exitCurrency.balanceOf(address(this));
_exit.claim(tokenBalance, address(this));
uint256 received = _exitCurrency.balanceOf(address(this)) - before;
require(received >= _minCurrencyAmount, "received less than _minCurrencyAmount");
uint256 basePayout = _scaleToDecimals(
(basePrice * tokenBalance) / 10 ** token.decimals(),
IERC20Metadata(address(_exitCurrency)).decimals()
);
uint256 carry = basePayout < received ? received - basePayout : 0;
_settle(carry, _exitCurrency);
}

/**
* @notice Returns the number of lead investors.
* @return length of the leadInvestors array
*/
function getLeadInvestorsCount() external view returns (uint256) {
return leadInvestors.length;
}
}
4 changes: 2 additions & 2 deletions contracts/Crowdinvesting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ contract Crowdinvesting is
require(_arguments.tokenPrice != 0, "_tokenPrice needs to be a non-zero amount");
require(_arguments.maxAmountOfTokenToBeSold != 0, "_maxAmountOfTokenToBeSold needs to be larger than zero");
require(
_arguments.token.allowList().map(address(_arguments.currency)) == TRUSTED_CURRENCY,
_arguments.token.allowList().map(address(_arguments.currency)) & TRUSTED_CURRENCY == TRUSTED_CURRENCY,
"currency needs to be on the allowlist with TRUSTED_CURRENCY attribute"
);
currencyReceiver = _arguments.currencyReceiver;
Expand Down Expand Up @@ -417,7 +417,7 @@ contract Crowdinvesting is
require(address(_currency) != address(0), "currency can not be zero address");
require(_tokenPrice != 0, "_tokenPrice needs to be a non-zero amount");
require(
token.allowList().map(address(_currency)) == TRUSTED_CURRENCY,
token.allowList().map(address(_currency)) & TRUSTED_CURRENCY == TRUSTED_CURRENCY,
"currency needs to be on the allowlist with TRUSTED_CURRENCY attribute"
);

Expand Down
Loading
Loading