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:

VariableDescription
managerManager of the fund
titleTitle of the fund
descriptionDescription of the fund
tokenPricePrice of the issued token; uses NAV for the waiting list mechanism and the result of auctions in funds based on dutch auctions.
tokensPerInvestorToken cap per investor; How many tokens a qualified investor can buy at once.
timeToBuyInHoursTime slot per investor; How long a qualified investor has time to decide wheter to invest or not.
startDateStart date of the fund; used for waiting list mechanism.
isDutchAuctionTo determine if the fund uses a dutch auction or a waiting list mechanism.
waitingListWaiting list of the fund; used to determine the position of the qualified investor in the fund.
auctionsAuctions of the fund; used with the struct Auction to have a collection of dutch auctions in the fund.
sellingsSelling positions of the fund; used with the struct Selling to have a collection of selling positions in the fund based on waiting list mechanism.
whiteListedInvestorsMapping 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:

EventsDescription
BuyTokensEmit event after buying tokens used in dutch auctions and waiting list mechansim.
CapitalCallEmit event after further tokens are minted.
SetNewTokenPriceEmit event after setting new token price for issued tokens in dutch auction or NAV in waiting list mechanism.
SetNewTokenCapEmit 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.

FunctionsDescription
withdrawOnly manager can withdraw the money from the contract.
addInvestorOnly manager can add an investor to the white-list.
removeInvestorOnly manager can remove an investor from the white-list.
mintNewTokensOnly manager can mint new tokens.
setTokenPriceOnly manager can set the token price; uses NAV for the waiting list mechanism and the result of auctions in funds based on dutch auctions.
setTokenPerInvestorOnly manager can set the token cap per investor; How many tokens a qualified investor can buy at once.
setTimeSlotOnly 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.