Skip to content

Feat/token swap coinvestor#401

Open
malteish wants to merge 79 commits intodevelopfrom
feat/TokenSwapCoinvestor
Open

Feat/token swap coinvestor#401
malteish wants to merge 79 commits intodevelopfrom
feat/TokenSwapCoinvestor

Conversation

@malteish
Copy link
Collaborator

@malteish malteish commented Feb 23, 2026

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.


New contract: Exit

Allows token holders to redeem tokens for a fixed currency payout within a configurable duration after the exit date. Mirrors Distribution's claim overloads and factory pattern.


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.
Old Crowdinvestings and PrivateOffers will not work with the new AllowList format, as their TRUSTED_CURRENCY check will return false.

@openzeppelin-code
Copy link

openzeppelin-code bot commented Feb 23, 2026

Feat/token swap coinvestor

Generated at commit: 384c4a5ca27fb968af8241de91499d26e002b2f9

🚨 Report Summary

Severity Level Results
Contracts Critical
High
Medium
Low
Note
Total
2
2
0
4
28
36
Dependencies Critical
High
Medium
Low
Note
Total
0
0
0
0
0
0

For more details view the full report in OpenZeppelin Code Inspector

"@openzeppelin/contracts": "4.9.6",
"@openzeppelin/contracts-upgradeable": "4.9.6"
"@openzeppelin/contracts-upgradeable": "4.9.6",
"@safe-global/safe-contracts": "^1.4.1-2"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we deploy SafeContracts v1.3. But this shouldn't be an issue.

}

function claim(IERC1271 _holder, bytes32 _hash, bytes memory _signature, address _recipient) external {
require(_holder.isValidSignature(_hash, _signature) == 0x1626ba7e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Slack you said this is not complete. What is missing? Isn't this enough?

Copy link
Collaborator Author

@malteish malteish Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technical:
While this satisfies the standard, it

  1. doesn's have replay protection
  2. doesn't enforce _recipient being part of the hash, so adversaries monitoring the mempool could replace _recipient and steal the funds
  3. possibly more, as I haven't studied it at depth yet

Practical:
It is unclear in which situation this function would be beneficial over safe.execTransaction(), so I can't assess if it actually delivers that expected value.
Safe.execTransaction is the tried and tested, general-purpose solution, so ECR1271 would have to provide some significant benefit to justify the additional complexity and attack surface.
Any safe can claim through safe.execTransaction()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed to remove ERC1271 support in call today. c7fbc32

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants