Technical Documentation
This is the technical documentation of the CSAM project at the Blockchain Challenge 2022 at the University of Basel. The GitHub repository can be found here.
Factory Design Pattern
We decided to use a factory pattern to create our smart contracts for closed-end funds. The createCEF function will create a closed-end fund contract and push the contract instance into an array. We used this pattern to simplify the front-end logic. Getting all the closed-end fund contract addresses to display them in the front-end is as simple as calling the getCEFAddresses function.
1contract CEFFactory { 2 3 ClosedEndFund[] public deployedCEF; 4 5 function createCEF( 6 string memory _title, 7 string memory _description, 8 uint256 _tokenPrice, 9 uint256 _initialSupply, 10 uint256 _tokensPerInvestor, 11 bool _isDutchAuction, 12 uint256 _timeToBuyInHours, 13 address[] memory _whiteListedInvestors 14 ) public { 15 ClosedEndFund newCEF = new ClosedEndFund( 16 msg.sender, 17 _title, 18 _description, 19 _tokenPrice, 20 _initialSupply, 21 _tokensPerInvestor, 22 _isDutchAuction, 23 _timeToBuyInHours, 24 _whiteListedInvestors 25 ); 26 deployedCEF.push(newCEF); 27 } 28 29 function getDeployedCEF() public view returns (ClosedEndFund[] memory) { 30 return deployedCEF; 31 } 32} 33
ERC20 & Constructor
We decided to use an ERC20 token. The fund contract inherits all the functionalities from the ERC20 contract provided by openzeppelin. To deploy the fund contract, a few parameters are needed. The variables are explained in the chapter Variables & Events. To whitelist investors, we used a for loop to iterate over the array of addresses provided by the contract deployer and add them to the whiteListedInvestors variable.
1contract CEFToken is ERC20 { 2 constructor(address _manager, uint256 initialSupply) 3 ERC20("CEF TOKEN", "CEF") 4 { 5 _mint(_manager, initialSupply); 6 } 7} 8 9contract ClosedEndFund is CEFToken { 10 ... 11 constructor( 12 address _manager, 13 string memory _title, 14 string memory _description, 15 uint256 _tokenPrice, 16 uint256 _initialSupply, 17 uint256 _tokensPerInvestor, 18 bool _isDutchAuction, 19 uint256 _timeToBuyInHours, 20 address[] memory _whiteListedInvestors 21 ) CEFToken(_manager, _initialSupply) { 22 manager = _manager; 23 title = _title; 24 description = _description; 25 tokenPrice = _tokenPrice; 26 tokensPerInvestor = _tokensPerInvestor; 27 isDutchAuction = _isDutchAuction; 28 timeToBuyInHours = _timeToBuyInHours; 29 startDate = block.timestamp; 30 waitingList = _whiteListedInvestors; 31 32 // add investors to mapping (whitelisting) - O(n) linear algorithm - it's ok if amount of white-listed Investors is small 33 for (uint256 i = 0; i < _whiteListedInvestors.length; i++) { 34 address whiteListedInvestor = _whiteListedInvestors[i]; 35 whiteListedInvestors[whiteListedInvestor].whiteListed = true; 36 } 37 } 38 ... 39} 40
Variables & Events
The variables are descriped in the table below:
Variable | Description |
---|---|
manager | Manager of the fund |
title | Title of the fund |
description | Description of the fund |
tokenPrice | Price of the issued token; uses NAV for the waiting list mechanism and the result of auctions in funds based on dutch auctions. |
tokensPerInvestor | Token cap per investor; How many tokens a qualified investor can buy at once. |
timeToBuyInHours | Time slot per investor; How long a qualified investor has time to decide wheter to invest or not. |
startDate | Start date of the fund; used for waiting list mechanism. |
isDutchAuction | To determine if the fund uses a dutch auction or a waiting list mechanism. |
waitingList | Waiting list of the fund; used to determine the position of the qualified investor in the fund. |
auctions | Auctions of the fund; used with the struct Auction to have a collection of dutch auctions in the fund. |
sellings | Selling positions of the fund; used with the struct Selling to have a collection of selling positions in the fund based on waiting list mechanism. |
whiteListedInvestors | Mapping with addresss as key and Investor struct as value; used for white-listing and waiting list mechanism. |
The events are descriped in the table below:
Events | Description |
---|---|
BuyTokens | Emit event after buying tokens used in dutch auctions and waiting list mechansim. |
CapitalCall | Emit event after further tokens are minted. |
SetNewTokenPrice | Emit event after setting new token price for issued tokens in dutch auction or NAV in waiting list mechanism. |
SetNewTokenCap | Emit event after setting new token cap for waiting list mechanism. |
1contract ClosedEndFund is CEFToken { 2 // *** VARIABLES *** // 3 address public manager; // address of the fund manager 4 string public title; // name of the fund 5 string public description; // description of the fund 6 uint256 public tokenPrice; // in WEI 7 uint256 public tokensPerInvestor; // tokens that investor can buy at once 8 uint256 public timeToBuyInHours; // time slot to buy tokens 9 uint256 public startDate; // start date when CA is deployed 10 bool public isDutchAuction; // decide whether it's a dutch auction or waiting list mechanism 11 address[] public waitingList; // waiting list array with ordered position 12 13 // *** STRUCT *** // 14 struct Auction { 15 address seller; // addres of seller 16 uint256 amountToSell; // amount of tokens to sell 17 uint256 startingPrice; // starting price of auction 18 uint256 minimumPrice; // mimimun price of auction 19 uint256 startAt; // start date 20 uint256 expiresAt; // end date 21 bool completed; // true if the auction has already been closed 22 } 23 24 Auction[] public auctions; 25 26 // used for waiting list mechanism 27 struct Selling { 28 address seller; 29 uint256 amountToSell; // amount of tokens to sell 30 bool completed; // true if the auction has already been closed 31 } 32 33 Selling[] public sellings; 34 35 struct Investor { 36 bool whiteListed; // true if white-listed 37 uint timeLastBoughtTokens; // check for waiting list mechanism if investor bought in the last timeToBuyInHours 38 } 39 40 mapping(address => Investor) public whiteListedInvestors; 41 42 // *** EVENTS *** // 43 event BuyTokens( 44 address buyer, 45 address seller, 46 uint256 amountOfETH, 47 uint256 amountOfTokens 48 ); 49 event CapitalCall(uint256 amountOfTokens); 50 event SetNewTokenPrice(uint256 newTokenPrice); 51 event SetNewTokenCap(uint256 newTokenCap); 52 event SetNewTimeToBuyInHours(uint256 newTimeToBuyInHours); 53 ... 54} 55
Modifiers
The modifiers shown below in the code are used to restrict access to some of the functions. We wrapped the require statements into functions and then used in the modifier to reduce bytecode size. The modifiers are self-explanatory.
1contract ClosedEndFund is CEFToken { 2 ... 3 // *** MODIFIERS *** // 4 function _onlyManager() private view { 5 require(msg.sender == manager, "Only allowed for fund manager"); 6 } 7 8 modifier onlyManager() { 9 _onlyManager(); 10 _; 11 } 12 13 function _isWhiteListed(address _address) private view { 14 require( 15 whiteListedInvestors[_address].whiteListed || 16 (msg.sender == manager), 17 "You are not a white-listed investor nor the manager" 18 ); 19 } 20 21 modifier isWhiteListed(address _address) { 22 _isWhiteListed(_address); 23 _; 24 } 25 26 function _isAuction() private view { 27 require(isDutchAuction, "Functions only available for auctions"); 28 } 29 30 modifier isAuction() { 31 _isAuction(); 32 _; 33 } 34 35 function _isWaitingList() private view { 36 require( 37 !isDutchAuction, 38 "Functions only available for waiting list mechanism" 39 ); 40 } 41 42 modifier isWaitingList() { 43 _isWaitingList(); 44 _; 45 } 46 ... 47} 48
Dutch Auction
The dutch auction has mainly three functionalities. A qualified investor can start a dutch auction (startAuction), check the price of a particular auction (getAuctionPrice), and make a bid for an auction to buy tokens from other qualified investors (buyAuctionToken). The function buyIssuedAuctionTokensFromManager allows investors to buy issued tokens directly from the fund manager with the price the manager sets for the tokens (tokenPrice). We had to develop two functions to buy directly from the manager, one for contracts based on dutch auctions (buyIssuedAuctionTokensFromManager) and one for funds based on waiting list mechanisms (buyIssuedWaitingListTokensFromManager).
1contract ClosedEndFund is CEFToken { 2 ... 3 // *** DUTCH AUCTION *** // 4 5 // no restriction buying issued tokens 6 function buyIssuedAuctionTokensFromManager(uint256 _amountOfTokens) 7 public 8 payable 9 isWhiteListed(msg.sender) 10 isAuction 11 returns (uint256 tokenAmount) 12 { 13 require(msg.value > 0, "Send ETH to buy some tokens"); 14 require( 15 msg.value == _amountOfTokens * tokenPrice, 16 "Send right amount of ETH for the tokens" 17 ); 18 19 // check manager balance 20 uint256 managerBalance = this.balanceOf(manager); 21 require( 22 managerBalance >= _amountOfTokens, 23 "Fund manager has not enough tokens in its balance" 24 ); 25 26 // Transfer token to the buyer 27 _transfer(manager, msg.sender, _amountOfTokens); 28 29 // emit the event 30 emit BuyTokens(msg.sender, manager, msg.value, _amountOfTokens); 31 32 return _amountOfTokens; 33 } 34 35 function startAuction( 36 uint256 _amountToSell, 37 uint256 _startingPrice, 38 uint256 _minimumPrice, 39 uint256 _durationInMinutes 40 ) public isWhiteListed(msg.sender) isAuction { 41 // check amount of tokens 42 uint256 sellerBalance = this.balanceOf(msg.sender); 43 require( 44 sellerBalance >= _amountToSell, 45 "Seller has not enough tokens in its balance" 46 ); 47 48 // initalize auction 49 Auction memory newAuction = Auction({ 50 seller: msg.sender, 51 amountToSell: _amountToSell, 52 startingPrice: _startingPrice, 53 minimumPrice: _minimumPrice, 54 startAt: block.timestamp, 55 expiresAt: block.timestamp + _durationInMinutes * 1 minutes, 56 completed: false 57 }); 58 59 // push to auction array 60 auctions.push(newAuction); 61 } 62 63 function getAuctionPrice(uint256 index) 64 public 65 view 66 isAuction 67 returns (uint256) 68 { 69 Auction memory auction = auctions[index]; // access auction 70 uint priceGap = auction.startingPrice - auction.minimumPrice; 71 return ((auction.startingPrice - (priceGap * (block.timestamp - auction.startAt)/(auction.expiresAt - auction.startAt)))) * auction.amountToSell; 72 } 73 74 function buyAuctionToken(uint256 index) external payable isAuction { 75 Auction storage auction = auctions[index]; // access auction; storage because need to change variable 76 77 require(!auction.completed, "This auction is completed"); 78 require(block.timestamp < auction.expiresAt, "This auction has ended"); 79 80 uint256 price = getAuctionPrice(index); 81 require( 82 msg.value >= price, 83 "The amount of ETH sent is less than the price of token" 84 ); 85 86 auction.completed = true; // close auction 87 88 _transfer(auction.seller, msg.sender, auction.amountToSell); // transfer token 89 90 uint256 refund = msg.value - price; 91 if (refund > 0) { 92 payable(msg.sender).transfer(refund); // refund buyer if payed too much 93 } 94 95 payable(auction.seller).transfer(price); // transfer money to seller 96 } 97 ... 98} 99
Waiting List
The waiting list is considered when an investor wants to buy tokens from other qualified investors or directly from the manager. The logic to decide whether an investor is in the correct position on the waiting list, so eligible to buy, is predominantly determined by the two helper functions findIndexInArray and checkTimeRestriction. These two functions are explained below in the chapter Helper Functions.
1contract ClosedEndFund is CEFToken { 2 ... 3 // *** WAITING LIST MECHANISM *** // 4 5 function buyIssuedWaitingListTokensFromManager(uint256 _amountOfTokens) 6 public 7 payable 8 isWhiteListed(msg.sender) 9 isWaitingList 10 returns (uint256 tokenAmount) 11 { 12 require(msg.value > 0, "Send ETH to buy some tokens"); 13 require( 14 msg.value == _amountOfTokens * tokenPrice, 15 "Send right amount of ETH for the tokens" 16 ); 17 require( 18 tokensPerInvestor >= _amountOfTokens, 19 "Tokens per investor cap exceeded" 20 ); 21 22 // check manager balance 23 uint256 managerBalance = this.balanceOf(manager); 24 require( 25 managerBalance >= _amountOfTokens, 26 "Fund manager has not enough tokens in its balance" 27 ); 28 29 // check waiting list position of buyer 30 int256 idx = findIndexInArray(msg.sender); 31 require(idx >= 0, "Investor is not found in waiting list"); 32 33 // check time restriction 34 checkTimeRestriction(uint256(idx)); 35 36 // after checks: allow to buy 37 38 whiteListedInvestors[msg.sender].timeLastBoughtTokens = block.timestamp; 39 40 // Transfer token to the buyer 41 _transfer(manager, msg.sender, _amountOfTokens); 42 43 // emit the event 44 emit BuyTokens(msg.sender, manager, msg.value, _amountOfTokens); 45 46 return _amountOfTokens; 47 } 48 49 function sellWaitingListToken(uint256 _amountToSell) 50 public 51 isWhiteListed(msg.sender) 52 isWaitingList 53 { 54 // check amount of tokens 55 uint256 sellerBalance = this.balanceOf(msg.sender); 56 require( 57 sellerBalance >= _amountToSell, 58 "Seller has not enough tokens in its balance" 59 ); 60 61 // initalize selling 62 Selling memory newSelling = Selling({ 63 seller: msg.sender, 64 amountToSell: _amountToSell, 65 completed: false 66 }); 67 68 // push to sellings array 69 sellings.push(newSelling); 70 } 71 72 function buyWaitingListToken(uint256 index, uint256 _amountOfTokens) 73 public 74 payable 75 isWhiteListed(msg.sender) 76 isWaitingList 77 returns (uint256 tokenAmount) 78 { 79 Selling storage selling = sellings[index]; // access selling; storage because need to change variable 80 require(!selling.completed, "This selling is completed"); 81 require( 82 tokensPerInvestor >= _amountOfTokens, 83 "Tokens per investor cap exceeded" 84 ); 85 require(msg.value > 0, "Send ETH to buy some tokens"); 86 require( 87 msg.value == _amountOfTokens * tokenPrice, 88 "Send right amount of ETH for the tokens" 89 ); 90 91 // check investors balance 92 uint256 sellerBalance = this.balanceOf(selling.seller); 93 require( 94 sellerBalance >= _amountOfTokens, 95 "Seller has not enough tokens (anymore) in its balance" 96 ); 97 98 // check waiting list position 99 int256 idx = findIndexInArray(msg.sender); 100 require(idx >= 0, "Investor is not found in waiting list"); 101 102 // check time restriction 103 checkTimeRestriction(uint256(idx)); 104 105 // after checks: allow to buy 106 107 // set time of buying for investor 108 whiteListedInvestors[msg.sender].timeLastBoughtTokens = block.timestamp; 109 110 // set completed to true if all tokens are sold 111 selling.amountToSell -= _amountOfTokens; 112 if (selling.amountToSell <= 0) { 113 selling.completed = true; 114 } 115 116 _transfer(selling.seller, msg.sender, _amountOfTokens); // Transfer token to the buyer 117 118 emit BuyTokens(msg.sender, selling.seller, msg.value, _amountOfTokens); // emit the event 119 120 payable(selling.seller).transfer(msg.value); // transfer (exact) money to seller 121 122 return (_amountOfTokens); 123 } 124 ... 125} 126
Corporate Actions
The functionalites of the corporate actions are descriped in the table below.
Functions | Description |
---|---|
withdraw | Only manager can withdraw the money from the contract. |
addInvestor | Only manager can add an investor to the white-list. |
removeInvestor | Only manager can remove an investor from the white-list. |
mintNewTokens | Only manager can mint new tokens. |
setTokenPrice | Only manager can set the token price; uses NAV for the waiting list mechanism and the result of auctions in funds based on dutch auctions. |
setTokenPerInvestor | Only manager can set the token cap per investor; How many tokens a qualified investor can buy at once. |
setTimeSlot | Only manager can set the time slot per investor; How long a qualified investor has time to decide wheter to invest or not. |
1contract ClosedEndFund is CEFToken { 2 ... 3 // *** Corporate Actions *** // 4 5 // manager can withdraw to invest 6 function withdraw() public onlyManager { 7 uint256 contractBalance = address(this).balance; 8 require(contractBalance > 0, "Contract has no balance to withdraw"); 9 payable(msg.sender).transfer(contractBalance); 10 } 11 12 // add investor to white-list 13 function addInvestor(address _addressToWhitelist) public onlyManager { 14 require( 15 !whiteListedInvestors[_addressToWhitelist].whiteListed, 16 "Already white listed" 17 ); 18 whiteListedInvestors[_addressToWhitelist].whiteListed = true; 19 waitingList.push(_addressToWhitelist); 20 } 21 22 // remove investor from white-list 23 function removeInvestor(address _addressToBlacklist) public onlyManager { 24 require( 25 whiteListedInvestors[_addressToBlacklist].whiteListed, 26 "Already black listed" 27 ); 28 whiteListedInvestors[_addressToBlacklist].whiteListed = false; // kind of blacklist; not really removed 29 30 // remove from waiting list in an ordered way 31 int256 index = findIndexInArray(_addressToBlacklist); 32 require(index >= 0, "Investor is not found in waiting list"); 33 for (uint256 i = uint256(index); i < waitingList.length - 1; i++) { 34 waitingList[i] = waitingList[i + 1]; 35 } 36 delete waitingList[waitingList.length - 1]; 37 } 38 39 // capital call 40 function mintNewTokens(uint256 _amountOfTokens) 41 public 42 onlyManager 43 returns (uint256) 44 { 45 _mint(manager, _amountOfTokens); // mint new tokens 46 emit CapitalCall(_amountOfTokens); // emit the event 47 return _amountOfTokens; 48 } 49 50 // set new token price 51 function setTokenPrice(uint256 _newTokenPrice) 52 public 53 onlyManager 54 returns (uint256) 55 { 56 tokenPrice = _newTokenPrice; // set new token price 57 emit SetNewTokenPrice(tokenPrice); // emit the event 58 return tokenPrice; 59 } 60 61 // set new token per investor 62 function setTokenPerInvestor(uint256 _newTokenCap) 63 public 64 onlyManager 65 returns (uint256) 66 { 67 tokensPerInvestor = _newTokenCap; // set new token price 68 emit SetNewTokenCap(tokenPrice); // emit the event 69 return tokensPerInvestor; 70 } 71 72 // set new time slot 73 function setTimeSlot(uint256 _NewTimeToBuyInHours) 74 public 75 onlyManager 76 returns (uint256) 77 { 78 timeToBuyInHours = _NewTimeToBuyInHours; // set new token price 79 emit SetNewTimeToBuyInHours(timeToBuyInHours); // emit the event 80 return timeToBuyInHours; 81 } 82 ... 83} 84
Helper Functions
The two helper functions findIndexInArray and checkTimeRestriction are used to determine if an investor is in the correct waiting list position and, therefore, allowed to buy. The idea behind it is to check where the index of the investor on the waiting list is and then compare it with time components. In order to avoid shifting the waiting list array, the modulo operator is used. The getSummary function is a helper function for simplifying the front-end. It allows to fetch all the information from a single contract.
1contract ClosedEndFund is CEFToken { 2 ... 3 //*** Helper ***// 4 5 function findIndexInArray(address _investor) 6 public 7 view 8 isWaitingList 9 returns (int256) 10 { 11 for (uint256 i = 0; i < waitingList.length; i++) { 12 if (waitingList[i] == _investor) { 13 return int256(i); 14 } 15 } 16 return -1; 17 } 18 19 function checkTimeRestriction(uint256 idx) 20 public 21 view 22 isWaitingList 23 { 24 uint256 timeEnd = startDate + 25 timeToBuyInHours * 26 60 * 27 60 * 28 waitingList.length; // end time of array calculated from startDate (one circle) 29 30 uint256 timeNow = (block.timestamp - startDate)%(timeEnd-startDate)+startDate; // time now calculated with modulo 31 32 uint256 timeToBuyStart = startDate + 33 (uint256(idx)) * 34 timeToBuyInHours * 35 60 * 36 60; // time to buy: slot start for investor 37 38 uint256 timeToBuyEnd = startDate + 39 (uint256(idx) + 1) * 40 timeToBuyInHours * 41 60 * 42 60; // time to buy: slot end for investor 43 44 // check if investor bought in the last x hours 45 require( 46 !(whiteListedInvestors[msg.sender].timeLastBoughtTokens > block.timestamp - timeToBuyInHours * 60 * 60), 47 "Investor bought tokens already. Wait until your next turn" 48 ); 49 50 // if investor still needs to wait 51 require( 52 !(timeToBuyStart > timeNow), 53 "Investors needs to wait. It's too early to buy." 54 ); 55 56 // if investor is too late 57 require( 58 !(timeNow > timeToBuyEnd), 59 "Investors needs to wait. It's too late to buy." 60 ); 61 } 62 63 function getSummary() 64 public 65 view 66 returns ( 67 address, 68 string memory, 69 string memory, 70 uint256, 71 uint256, 72 uint256, 73 uint256, 74 bool, 75 address[] memory, 76 Auction[] memory, 77 Selling[] memory 78 ) 79 { 80 return ( 81 manager, 82 title, 83 description, 84 tokenPrice, 85 tokensPerInvestor, 86 timeToBuyInHours, 87 startDate, 88 isDutchAuction, 89 waitingList, // incl. white-listed investors 90 auctions, 91 sellings 92 ); 93 } 94 95 //*** ERC20 OVERRIDE ***// 96 97 // override to avoid transfering shares outside of fund 98 function transfer(address _recipient, uint256 _amount) 99 public 100 override 101 onlyManager 102 returns (bool) 103 { 104 _transfer(msg.sender, _recipient, _amount); 105 return true; 106 } 107 108 function approve(address spender, uint256 amount) 109 public 110 override 111 onlyManager 112 returns (bool) 113 { 114 _approve(msg.sender, spender, amount); 115 return true; 116 } 117} 118
Front-End
The front-end is the web application that allows the investor and the manager to interact with the contracts in user friendly way. The framework we used is Nextjs. Additionally, we injected Web3 library to connect with the blockchain. Fetching the smart contracts and deploying them works. When it comes to the management of the funds, we decided not to implement the functionalities, as the visitors at the Gala Event will not be able to trade the tokens. Instead, we implemented the funds pre-filled pages for a fund with a dutch auction and a fund based on a waiting list mechanism. These funds appear in the exploration section, as soon as the front-end sees that no Metamask connection is established.
Credits & Disclosure
To learn Solidity and the concepts behind it we attended the class from Prof. Dr. Fabian Schär "Smart Contracts and Decentralized Blockchain Applications" and a course on Udemy "Ethereum and Solidity: The Complete Developer's Guide" by Stephen Grider. Concepts like "Factory", "getSummary" and how to implement a front-end with Nextjs were demonstrated in the Udemy course. Concepts like block.timestamp was demonstrated in Prof. Dr. Fabian Schär's course. Implementation of the code are original from the CSAM team. We are not aware of any copies. We are not aware of any third party code, except for the credits below.