Code Monkey home page Code Monkey logo

aelin's Introduction

Aelin Overview

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.

Key terms

  • 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 15%.

User journeys

SPONSOR

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 token
  • string memory _symbol used as part of the symbol of the ERC20 pool and deal token
  • uint _purchaseTokenCap- the max amount of purchase tokens that can be used to buy pool tokens. if set to 0 the deal is uncapped
  • address _purchaseToken the purchase token used to buy the pool token
  • uint _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 funds
  • uint _sponsorFee- an optional fee from the sponsor set between 0 and 15%
  • 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 15% (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 and dealNotCreated only the sponsor may call this method before a deal is created

Arguments:

  • address _underlyingDealToken the underlying deal token a purchaser receives upon vesting
  • uint _purchaseTokenTotalForDeal the total amount of purchase tokens that can be converted for the deal tokens
  • uint _underlyingDealTokenTotal the total amount of underlying deal tokens all purchasers receive upon vesting
  • uint _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 occurs
  • address _holder the entity or individual with whom the sponsor agrees to a deal
  • uint _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 period
  • uint _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

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 the totalSupply 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) or AelinPool.acceptMaxDealTokens() while the block.timestamp < proRataRedmeptionExpiry. In this case calling max will send all of their purchase tokens to the HOLDER, send 2% of the deal tokens to the AELIN_REWARDS address for Aelin token stakers, and an optional % from 0 to 15 to the SPONSOR which was set as the sponsorFee 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 the block.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 the block.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() or AelinPool.acceptDealTokens(uint poolTokenAmount) while the block.timestamp < openRataRedmeptionExpiry

HOLDER

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

Development workflow and Integration Test Setup

The integration tests require that hardhat run a fork of mainnet (see docs). For this to work you must do the following:

  1. setup an Alchemy account (it is free)
  2. create an app and get the https key
  3. 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

  1. export KOVAN_PRIVATE_KEY=... any private key with some kovan ETH on it for deployment
  2. export ALCHEMY_API_KEY=... the same key at the end of the ALCHEMY_URL environment variable but it needs to be in its own environment variable.

Deploying Aelin

NOTE: Steps 1 and 2 are repo setup steps that should not be needed but have not been refactored out.

  1. export ALCHEMY_API_KEY (just the key part) from step 2 which is needed in running integration tests.

  2. grab an Ethereum private key and get some Kovan ETH on it if using KOVAN. export KOVAN_PRIVATE_KEY=<key>. NOTE we might need some additional setup around hardhat for deploying to Optimism too

  3. npm run deploy-deal:<network> - take the address of the deployed deal from the CLI and paste it in scripts/deploy-pool-factory.js variable dealLogicAddress

  4. npm run deploy-pool:<network> - take the address of the deployed pool from the CLI and paste it in scripts/deploy-pool-factory.js variable poolLogicAddress

  5. npm run deploy-owner-relay-on-optimism - to deploy the Optimism Bridge (OwnerRelayOnOptimism.sol) and paste it in scripts/deploy-optimism-treasuty.js variable owner and also paste it in scripts/deploy-owner-relay-on-ethereum.js variable relayOnOptimism and also paste it in scripts/optimism-bridge-set-contract-data.js variable bridgeAddress. Note the private key used to deploy this contract will be the temporary owner of this contract for the amount of time specified in scripts/deploy-owner-relay-on-optimism.js variable ownershipDuration. You will need to use this same private key as the signer in scripts/helpers/optimism-bridge-set-contract-data.js. I have not run this yet but using ethers from hardhat and having the deployer sign it is prob the best move here.

  6. npm run deploy-optimism-treasury - take the address of the deployed deal fee/ AELIN token treasury address from the CLI and paste it in the scripts/deploy-pool-factory.js variable rewardsAddress and also paste it in scripts/deploy-aelin-token.js variable optimismTreasury

  7. npm run deploy-owner-relay-on-ethereum - take the address of the deployed bridge and paste it in scripts/optimism-bridge-set-contract-data.js variable relayOnEthereum

  8. npm run optimism-bridge-set-contract-data to set the contract data for the bridge so it is aware of the Ethereum bridge address. Note that this will finalize setup of the contract. you still want to test this bridge more while the ownershipDuration used in step 5 is still active.

8a. (testing) transfer some funds to the Optimism Treasuty and try calling the direct relay method on the Optimism Bridge as the temporary owner (deployer) npm run optimism-bridge-set-contract-data can make this call if you comment out the top part and uncomment the bottom. This script makes sure the temp owner can move the funds during their ownershipDuration in case something goes wrong.

8b. (testing) try transfering some funds out of the Optimism Treasury using the relay calls from Ethereum L1 Bridge (TODO - write this script from L1 that makes a call to the bridge with the proper encoding)

8c (testing) call nominateNewOwner on the Optimism Treasury from the Ethereum Bridge to make sure that it is working so we can soon transfer ownership of the Treasury to a L2 multisig controlled by Aelin Council

  1. npm run deploy-pool-factory:<network>

  2. npm run deploy-aelin-token:optimism to deploy the AELIN token and send all the tokens to the OptimismTreasury deployed contract;

  3. Create a vAELIN ERC20 token on Optimism by calling npm run deploy-virtual-aelin:optimism and paste this vAELIN ERC20 address in scripts/helpers/dist-addresses.json under optimism.VirtualAelinToken json field

  4. Run the historical staking data script. You need to have an Optimism archive node running to get the totalL2Debt, lastDebtLedgerEntryL2 for the block we are looking to capture the data at 1231113 in this case. Daniel sent me this link yesterday so we can run an Optimism archive node locally while doing this. It syncs quickly apparently (https://github.com/optimisticben/op-replica). On the other hand, I took a snapshot today at block 13839440 in case the archive node is not working. at that block the totalL2Debt is 44623051603213924679706746 and the lastDebtLedgerEntryL2 is 10432172923357179928181650. we can use the corresponding L1 block for this later snapshot (it is likely in the area of block 13839700???). I have the right L1 block hardcoded if the OP archive node works.

  5. Double check that the script was run properly and the distribution scores are ready in scripts/helpers/staking-data.json; this is a json file with address key and score fields such as: { "0x829BD824B016326A401d083B33D092293333A830": 5.861 }

  6. npm run deploy-distribution:optimism to build the distribution merkle tree from the list of scores and deploy the distribution contract with the merkle root. Make sure that the scripts/helpers/optimism/dist-hashes.json file saves properly as users will need the merkle root leaves from this file in order to make their claims. we can have them download this entire file and find their item in the long array but only when they go to the claim screen.

NOTE that you will now have a working set of Aelin Contracts sending deal fees to a treasury contract on L2 which is controlled by a multisig on L1 until gnosis is deployed and can transfer ownership to a L2 multisig. The treasury contract will also have all the AELIN tokens in it, ready to be distributed from the L1 multisig. We need to make sure that the L1 multisig can transfer all of the funds and change owners to the future L2 multisig.

We can do the Balancer pool at another point in time.

Outstanding questions: can you just deploy using hardhat with --network optimism

aelin's People

Contributors

0xisuruss avatar 0xlinus avatar aelin-xyz avatar alextheboredape avatar cranium7811 avatar drptbl avatar snc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

aelin's Issues

NFT Gated Deals: Add support for POAP like contracts

Right now, we support ERC721 contracts but we do not support contracts that mint multiple collections on a single contract address like POAP. We need to modify NFT gated deals to add support for POAP so that you can whitelist different ranges of token ids on a contract instead of supporting the entire contract. this can be used to support sub collections of any ERC721 that use consecutive IDs

Update solidity to latest version

We can just do this for all the contracts related to upfront deals for now. the Aelin Pools is less popular and we will slowly stop supporting these contracts unless we see an increase in demand for this product in the coming months

NOTE we might want some of the standalone contracts in the repo updated as well depending if we will reuse them. TODO: look into which contracts outside of the AelinUpFrontDeal.sol and related libraries and contracts need to be updated

Remove wallet can only submit once logic from NFT Gated Deals

if (!nftCollectionRules.purchaseAmountPerToken && nftCollectionRules.purchaseAmount > 0) {
require(!_data.nftWalletUsedForPurchase[_collectionAddress][msg.sender], "wallet already used for nft set");

this logic in AelinNFTGating.sol is something that we have never used and the logic does not make sense. We should remove it from the logic set. There is the option to have it so that a wallet can only participate once and all their NFTs become blacklisted. the reason it doesn't make sense is we need to pass in the token ids to be blacklisted. if a pool is used with this setting it just incentivizes people to send their NFTs to different wallets which is not ideal. we should just remove it

Future: Implement Delegate Cash and Warm XYZ

NOTE that we might not want to start on this ticket until they are supported across every EVM network we support. Delegate cash looks like they have better documentation and support more EVM networks at the moment. We can wait until they support every network we are on

Improve upfront deal workflow

Right now the contract workflow works for every edge case but it is not optimized for some common use cases. This is really bad for mainnet deals when gas is expensive. we have seen some users unhappy with the transaction costs even when they are necessary for a given pool

  1. if there is no deallocation allowed and no purchase raise minimum but there is a vesting schedule we should be able to skip the settle transaction
  2. if there is no deallocation allowed and no purchase raise minimum and there is also no vesting period, we can just exchange the tokens directly when they send the funds in. no need for settling or claiming.
  3. if deallocation is allowed but there is no vesting period, we can exchange the tokens when settle is called

Laminar Allocation

Need to implement https://aelips.aelin.xyz/aelips/aelip-34/.

This AELIP proposes to create a new allocation mechanism, commonly referred to as a "Laminar allocation", for investors in NFT gated deals when there is excess interest in a capped deal. In the proposed allocation, smaller investors are deallocated less than larger investors to ensure a wider distribution of deal tokens.

Please note that for this feature to be implemented and the "cut off value" to be calculated, all the deposits need to be sorted in ascending order. This is not very gas friendly knowing that a pool could get 1000+ deposits. A first guess would be to maintain and update a list every time a new deposit is made. Any element (deposit/wallet) of this list needs to be easily accessible to make sure we don't run out of gas. An implementation of a LinkedList in Solidity could definitely work.

Owner can bypass the restriction of address(0) and can assign owner as address(0) to contract. (business logic bug)

Hey Aelin team,

I hope you are fine and doing good, I have been reading your contracts since I started looking at your audits done by different individuals and companies, many things are being pointed out by the auditors. However, one Medium BUG I would like to address here at https://github.com/AelinXYZ/aelin/blob/53710152d3746cbfc5337e88a3f01694d5b26999/contracts/Owned.sol.

BUG:

The contract work as expected as at the start it checks for address(0), owner can never be address(0) fair enough. However, this restriction is lacking when changing the owner functionality comes in at nominateNewOwner.

In order to test it, try to deploy the contract with address(0) which would revert due to require(_owner != address(0), "Owner address cannot be 0");, now deploy the contract normally and pass address(0) at nominateNewOwner function. This makes the possibility of making invalid address the owner of the contract.

Possible Recommendation:

add check at address(0) as:

function nominateNewOwner(address _owner) external onlyOwner {
        require(_owner != address(0), "No zero address");
        nominatedOwner = _owner;
        emit OwnerNominated(_owner);
    }

Thank you,

Regards,
newfolder

Update NftCheck.sol in the libraries

Hello developers. We are utilizing some automated tools to detect potential dependency issues. If there are any inaccuracy, we would greatly appreciate your corrections and feedback.

We noticed that there is a known problem/bug in the contract libraries/NftCheck.sol. The NftCheck contract use solidity 0.8.6 and abi.decode. According to GHSA-qh9x-gcfh-pcrw, this contract may revert instead of returning false. The influenced functions are functions invoking _supportsInterface. It could be upgraded to the fixed version or the latest like the OZ library.

This issue may not directly cause security risks, but it can influence users by malicious data or someone who forked this repository. We known that possible fixes may have to be in the next major version. However, we hope that the security advisory could be in contract comments or documentation to facilitate users' understanding of potential issues and monitoring of actual behaviors.

Multiple vesting schedules

Implementation of AELIP-30

This AELIP proposes adding the ability for deal creators to configure multiple vesting schedules within the same deal to be sold at different prices. The purchaser will be able to choose the vesting schedule they prefer for the corresponding token price.

An old PR was started here but never finished.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.