Aelin is a fundraising protocol built on Ethereum. A sponsor goes out and announces they are raising a pool of capital with a purchase expiry period. The sponsor can create a public or private pool. If it is a public pool, qnyone with an internet connection, aka the purchaser, can contribute funds (e.g. sUSD) to the pool during the purchase expiry period; after the purchase expiry period, the funds are locked for a time duration period while the sponsor searches for a deal. Private pools are reserved for an allow list of addresses with specified investment amounts.
If the sponsor finds a deal with the holder of a tokenized asset after the purchase expiry period, the sponsor announces the deal terms to the purchasers and then the holder has a specified time period to send the underlying deal tokens/ tokenized assets to the contract. If the funds are sent, the purchasers can convert their pool tokens (or a partial amount) to deal tokens, which represent a claim on the underlying deal token. Pool tokens are transferable until the deal is created and fully funded. After the deal is funded, pool tokens must be either accepted or withdrawn for the purchase token. If the holder does not send the underlying deal tokens in time, the sponsor can create a new deal for the pool.
If the purchasers are not interested in the underlying deal token they are welcome to reject the deal and withdraw their capital after the deal terms are announced. Also if a deal is not found then the purchasers can take their money back at the end of the pool duration.
The deal token is an ERC20 that might include a vesting schedule or not to claim the underlying deal token, depending upon the deal. Since the unvested underlying deal tokens are wrapped as an ERC20 they may be sold or traded before the vesting period is over. However, all vested tokens will be claimed and the respective deal tokens burned before any transfer occurs.
-
Sponsor
- the entity or individual raising capital to pursue a deal -
Holder
- the entity or individual seeking capital in exchange for an underlying deal token they hold -
Purchaser
- the entity or individual providing capital in exchange for a possible investment opportunity -
Purchase token
- the token that the sponsor requires the Purchaser use to buy into the pool -
Pool token
- the wrapped token received by the Purchaser as an indicator of their contribution to the pool. Represents a claim on the purchase token if the purchaser is not interested in the deal. -
Deal token
- the wrapped token received by the Purchaser as an indicator of their acceptance of the deal. Optionally wraps the underlying deal token in a vesting schedule. -
Underlying deal token
- the final token given to the purchaser in exchange for their purchase tokens at the end of the vesting period if they accepted the deal. -
Aelin Fee
- 2% fee to the protocol taken from every purchaser when they accept a deal. -
Sponsor Fee
- optional fee set by the sponsor when they announce the pool. can range from 0 to 98%.
SPONSOR STEP 1 (Create a Pool): Create a pool by calling AelinPoolFactory.createPool(...)
Arguments:
string memory _name
used as part of the name of the ERC20 pool and deal tokenstring memory _symbol
used as part of the symbol of the ERC20 pool and deal tokenuint _purchaseTokenCap
- the max amount of purchase tokens that can be used to buy pool tokens. if set to 0 the deal is uncappedaddress _purchaseToken
the purchase token used to buy the pool tokenuint _duration
the duration of the pool which starts after the purchase expiry period ends. if no deal is created by the end of the duration, the purchaser may withdraw their fundsuint _sponsorFee
- an optional fee from the sponsor set between 0 and 98%uint _purchaseDuration
the amount of time a purchaser has to buy a pool token before the sponsor can create the deal
Requirements:
- the
_duration
must be <= 1 year (revert) - the
_purchaseDuration
must be >= 30 minutes and <= 30 days (revert) - the
_sponsorFee
must be between 0% and 98% (revert)
NOTE if SPONSOR never finds a deal this is the end of their journey and the PURCHASER can retrieve their purchase tokens at the end of the _duration
If a deal is found, the SPONSOR must wait for PURCHASER step 1 (Enter the Pool)
to be completed and the purchase expiry period to end before going to create a deal in step 2.
SPONSOR STEP 2 (Create a Deal): Creates a deal by calling AelinPool.createDeal(...)
Modifiers:
onlySponsor
anddealNotCreated
only the sponsor may call this method before a deal is created
Arguments:
address _underlyingDealToken
the underlying deal token a purchaser receives upon vestinguint _purchaseTokenTotalForDeal
the total amount of purchase tokens that can be converted for the deal tokensuint _underlyingDealTokenTotal
the total amount of underlying deal tokens all purchasers receive upon vestinguint _vestingPeriod
the total amount of time to fully vest starting at the end of the vesting cliff (vesting is linear for v1)uint _vestingCliff
the initial deal token holding period where no vesting occursaddress _holder
the entity or individual with whom the sponsor agrees to a dealuint _holderFundingDuration
the amount of time a holder has to fund the deal before the proposed deal expires
NOTE please be sure to understand how the 2 redemption periods work outlined below:
uint _proRataRedemptionPeriod
the time a purchaser has to redeem their pro rata share of the deal. E.g. if the_purchaseTokenTotalForDeal
is only 8M sUSD but the pool has 10M sUSD (4:5) in it then for every $1 the purchaser invested they get to redeem $0.80 for deal tokens during this period. If the proRataConversion rate is 1:1 there is no open redemption perioduint _openRedemptionPeriod
is a period after the_proRataRedemptionPeriod
when anyone who maxed out their redemption in the_proRataRedemptionPeriod
can use their remaining purchase tokens to buy any leftover deal tokens if some other purchasers did not redeem some or all of their pool tokens for deal tokens
Requirements:
- the
block.timestamp >= purchaseExpiry
(revert) - the
_holderFundingDuration
must be >= 30 minutes and <= 30 days (revert) - the
_proRataRedemptionPeriod
must be >= 30 minutes and <= 30 days (revert) - the
_openRataRedemptionPeriod
must be >= 30 minutes and <= 30 days, If the proRataConversion rate is not 1:1, otherwise it must be 0 (revert) - the
_purchaseTokenTotalForDeal
converted to 18 decimals must be <= totalSupply of pool tokens (revert)
NOTE the sponsor journey has ended IF the holder funds the deal. From here the next step is HOLDER step 1 (Fund the Deal)
. However, if the holder does not fund the deal a sponsor can create a new deal for the pool by calling AelinPool.createDeal(...)
again. There is always only 1 deal per pool.
EXTRA_METHODS
: only the sponsor may also call setSponsor()
followed by acceptSponsor()
from the new address at any time to update the sponsor address for a deal
PURCHASER STEP 1 (Enter the Pool): Purchase pool tokens by calling AelinPool.purchasePoolTokens(...)
.
Arguments:
uint _purchaseTokenAmount
- the amount of the purchase token to use to buy pool tokens
Requirements:
- the
_purchaseTokenAmount
when converted to 18 decimal format plus thetotalSupply
of the pool token must be <=poolTokenCap
unless the cap is set to 0 (revert) - the pool tokens must be purchased when
block.timestamp
<=purchaseExpiry
NOTE after PURCHASER step 1 (Enter the Deal)
is SPONSOR step 2 (Create the Deal)
and then HOLDER step 1 (Fund the Deal)
followed by PURCHASER step 2 (Accept or Reject the Deal)
. NOTE if a sponsor never creates a deal the purchaser can withdraw their funds the same way as if they reject the deal
PURCHASER STEP 2 (Accept or Reject the Deal): At step two the purchaser has 2 options: reject or accept the deal. At this point they can no longer transfer their pool tokens.
OPTION 1 - REJECT: Rejects a portion of or all of the deal offered by calling AelinPool.withdrawMaxFromPool()
or withdrawFromPool(uint purchaseTokenAmount)
Arguments:
uint purchaseTokenAmount
used when withdrawing a specific amount and not all your tokens by calling the max function instead
Requirements:
block.timestamp > poolExpiry
the method can only be called after the pool has expired which can happen at the end of the_duration
or when the deal is created (revert)
OPTION 2 - Accept: NOTE the deal acceptance phase can have several steps under various circumstances outlined below
Accept when Conversion Ratio == 1:1 (e.g. a pool has $10M sUSD in it and the deal is for $10M sUSD)
-
PRO RATA PERIOD: The purchaser can either call
AelinPool.acceptDealTokens(uint poolTokenAmount)
orAelinPool.acceptMaxDealTokens()
while theblock.timestamp < proRataRedmeptionExpiry
. In this case calling max will send all of their purchase tokens to theHOLDER
, send 2% of the deal tokens to theAELIN_REWARDS
address forAelin
token stakers, and an optional % from 0 to 98 to theSPONSOR
which was set as thesponsorFee
in the pool creation at the beginning of the process. If not accepting max, any additional tokens may be withdrawn at any time -
OPEN REDEMPTION PERIOD: (n/a - since the ratio is 1:1 all purchasers have already had the chance to max their contributions)
Accept when Conversion Ratio is less than 1:1 (e.g. a pool has $10M sUSD in it but the deal is for $8M sUSD)
-
PRO RATA PERIOD:
-
DOES NOT MAX Accept. the purchaser only accepts a portion of their tokens by calling
AelinPool.acceptDealTokens(uint poolTokenAmount)
while theblock.timestamp < proRataRedmeptionExpiry
. They may withdraw their remaining amount at any time. E.g. a user who purchased $100 sUSD of pool tokens only accepts $50 instead of their full $80 allocation -
DOES MAX Accept: the purchaser accepts all of their deal tokens by calling
AelinPool.acceptMaxDealTokens()
while theblock.timestamp < proRataRedmeptionExpiry
. E.g. a user who purchased $100 sUSD of pool tokens accepts $80
-
-
OPEN REDEMPTION PERIOD:
-
DID NOT MAX ACCEPT: if the purchaser did not max out their allocation in the
proRataRedemptionPeriod
they are not eligible to participate in the open redemption period (revert) -
DID MAX ACCEPT: if the purchaser maxed their allocation they may redeem their remaining purchase tokens for deal tokens up until they have used all their funds or the deal cap has been reached. They can do this by calling
AelinPool.acceptMaxDealTokens()
orAelinPool.acceptDealTokens(uint poolTokenAmount)
while theblock.timestamp < openRataRedmeptionExpiry
-
HOLDER STEP 1 (Fund the Deal): After the deal has been created by the sponsor, the holder (or any address on behalf of the holder) funds the deal by calling AelinDeal.depositUnderlying(...)
Modifiers:
finalizeDepositOnce
once the full deal amount is deposited this method can no longer be called
Arguments:
uint _underlyingDealTokenAmount
the amount of the underlying deal token to deposit when calling this method. NOTE if the holder accidentally transfers the funds without using this method they can still call it with_underlyingDealTokenAmount
set to 0 to finalize the deal creation
The holder is nearly done. The only remaining step for them is to withdraw any excess funds accidentally deposited now or at the end of the expiry period if not all the deal tokens have been redeemed by purchasers.
After calling AelinDeal.depositUnderlying(...)
, the deal proRataDealRedemption
period starts and Purchaser step 2
begins
EXTRA_METHODS
: only the holder may also call setHolder()
followed by acceptHolder()
from the new address at any time to update the holder address for a deal
The integration tests require that hardhat run a fork of mainnet (see docs). For this to work you must do the following:
- setup an Alchemy account (it is free)
- create an app and get the
https
key export ALCHEMY_URL=https://eth-mainnet.alchemyapi.io/v2/<key>
NOTE: the first time you run the test it will be slow. Hardhat caches the requests to Alchemy, so it will be faster on subsequent runs
Environment variables needed for the codebase in addition to ALCHEMY_URL
export KOVAN_PRIVATE_KEY=...
any private key with some kovan ETH on it for deploymentexport ALCHEMY_API_KEY=...
the same key at the end of theALCHEMY_URL
environment variable but it needs to be in its own environment variable.
- export ALCHEMY_API_KEY (just the key part) from step 2 in running integration tests.
- grab an Ethereum private key and get some Kovan ETH on it.
export KOVAN_PRIVATE_KEY=<key>
. npm run deploy-deal:<network>
- take the address of the deployed deal from the CLI and paste it in theAelinPoolFactory
asaddress constant AELIN_DEAL_LOGIC = ...
npm run deploy-pool:<network>
- take the address of the deployed pool from the CLI and paste it in theAelinPoolFactory
asaddress constant AELIN_POOL_LOGIC = ...
npm run deploy-pool-factory:<network>