@vidy-dev/ethereum-ico
v1.0.3
Published
Contracts related to the ICO sales
Downloads
30
Readme
ethereum-ico
Contracts for the VidyCoin ICO: vault, sales, whitelists
Install
$ yarn add @vidy-dev/ethereum-ico
or
$ npm install @vidy-dev/ethereum-ico
ICOVault
Some fundraising will be conducted outside the Ethereum blockchain, and so some of the 5.2 billion VidyCoins allocated for the ICO will be distributed according to terms enforced by paper contracts rather than the blockchain itself. The whitelisted pre-ICO and public ICO sales rounds will occur within the blockchain, however, and the ICOVault
contract will hold all ether raised by those rounds in escrow until the ICO period is over or the soft cap is reached. If the ICO is unsuccessful, the ether is made available to the investors who initially deposited it. If the soft cap is reached, the ether is transferred to the InvestorWallet
, in a lump sum at the end of the sale or, optionally, in increments as the sale continues.
The ICOVault
serves multiple purposes:
- Holding ether in escrow, only allowing us to retrieve it if the fundraising soft cap was reached. This establishes a trustless guarantee that refunds will be provided if the ICO fails.
- Providing one single canonical reference point to determine if the ICO soft cap has been reached (the ICOVault has a balance of deposited ether, and a publicly readable goal balance).
- Enforcing start / stop dates on the ICO in a trustless way (the expiration date of the ICOVault cannot be altered once the VidyCoin sale has begun).
- Recording the total amount of ether contributed by any given investor across the trustless sales rounds, preventing refunds exceeding this value.
ICOVault Contract Code
ICOVault.sol
is the contract representing the fundraising vault. Through multiple inheritance it provides an implementation of the abstract contract in Vault.sol
. The Vault
interface is heavily influenced by the example crowdsale vault provided by openzeppelin-solidity
: https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/crowdsale/distribution/utils/RefundVault.sol; changes were made in the behavior of ICOVault
compared to this example in order to support specific requirements for our crowdsale. The main differences between our sale and the OpenZeppelin reference:
- Multiple sale contracts are authorized to make deposits into the vault (represented as a whitelist held by the vault).
- Only those sales contracts may initiate refunds (so that their own records can be updated at the same time).
- The ICOVault holds all the necessary information to determine the status of the VidyCoin ICO: how much has been raised on the blockchain thus far, the soft cap (goal) of the raise, on what date the ICO will finish, etc. That information is also publicly queriable.
- Under the appropriate circumstances, the vault's state can be changed to
Refunding
orClosed
by anyone. We intend to execute this transaction ourselves, but allowing anyone to do so establishes trustlessly that refunds will be provided in the event of an unsuccessful ICO.
The ICOVault descends from Ownable
, NoRemovalWhitelist
, and Schedulable
, three utility contracts. Ownable
is part of the OpenZeppelin suite and establishes one single Ethereum address as the "owner" of the contract, authorized to perform some specific set of functions. NoRemovalWhitelist
allows certain functions to be limited to a list of "whitelisted" addresses, a list which can be grown by the owner adding new addresses but not shrunk. Schedulable
gives the contract activation and expiration times, with specific behavior based on its current state. Although a Schedulable
contract can freely be rescheduled by its owner as long as it has not yet activated, once the activation time arrives, that time and the expiration time become immutable.
The main function of the vault is to store ether deposited by investors for the sale period, and once that sale period ends either forwarding it to a wallet owned by us or allowing investors to receive refunds (depending on whether the ICO reached its goal). However, we don't intend for investors to interact with the vault directly; they interact with specific sale contracts (each with different sale terms), with those sale contracts making deposits to the vault on behalf of investors, and initiated refunds from the vault when appropriate. To ensure that sales records are consistent with vault deposit records, only the sale contracts are allowed to invoke the deposit
and refund
vault methods, although the result of those methods (deposit records and events) are visible to anyone.
The vault, being the single deposit point for all token sales, is the contract which determines whether our ICO fundraising goal (soft cap) has been reached. If so, the ether raised will be transferred to the InvestorWallet
address; if not, refunds are made available to ICO customers. To guarantee this behavior, transition between vault states (Active -> Closed
or Active -> Refunding
) may be initiated by anyone, but only under specific contract conditions. This provides a trustless guarantee that refunds will be offered (authorizing refunds for an unsuccessful fundraise does not require a member of the Vidy team).
Ownership
The owner
of the vault is an address authorized to perform certain configuration function calls; it will most likely be the address responsible for deploying the ICOVault contract. The contract does not allow the owner to withdraw ether or change the intended recipient of the ether collected.
Abilities of the owner:
- Adding addresses to the deposit whitelist (token sale contracts will be added to the whitelist as those contracts are deployed).
- Rescheduling the vault's activation and expiration time if the ICO is delayed. Only allowed until the current activation time, at which point both values become immutable.
- Changing the fundraising goal within the range of the immutable minimum and maximum. Only allowed until the expiration time of the vault.
Whitelisting
Only whitelisted addresses may make deposits into, or initiate refunds out of, the ICOVault. In practice these whitelist addresses will be the addresses of sale contracts, each distributing VidyCoins under specific terms as a raise round of the ICO, and initiating refunds from the vault when requested and appropriate.
Scheduling
The activation and expiration times of the vault corresponding to the start and end times of the portion of the VidyCoin ICO performed on the Ethereum network: the pre-ICO raise and public ICO raise. Although the ICOVault can be rescheduled if necessary up until the beginning of the pre-ICO raise, once fundraising has begun and the vault activates, its expiration time is immutable.
The ICOVault can only be transitioned from an Active
state to Closed
or Refunding
after the expiration time.
Deposit Limits
The ICOVault allows minimum and maximum contributions from particular addresses. Deposits will be refused if smaller than the minimum amount, or if they would raise the total deposited by that account above the maximum.
If set, these limits are applied per-investor-address, meaning it would be possible to evade the contribution maximum by depositing from multiple wallets. This cannot be prevented by the blockchain itself; instead, it is the responsibility of off-blockchain KYC verification to ensure that a particular investor does not register multiple wallet addresses -- that each new account address added to the sale whitelists represents a new individual who has not previously registered an address.
Goal
The ICOVault has a publicly readable goal of ether raised, which the vault owner can alter within a prespecified range to account for fluctuations in ether value. The goal can only be adjusted up to the vault's expiration time, after which point the goal has either been reached, or it hasn't.
The VidyCoin offering will be performed in multiple stages, including fundraising that occurs outside the blockchain before the public ICO is opened. Because of this additional fundraising period, the goal
specified in the ICOVault
contract will be strictly less than the total ICO goal given in the whitepaper -- the ICOVault
goal will be set such that, when summed with the earlier fundraising, the total whitepaper goal is reached.
Deposits and Refunds
Deposits initiated by the whitelisted sale contracts are made in ether and accompanied by a depositor address; the ICOVault records the amount received as belonging to that address. Deposits can only be made while the vault is an Active
state.
Refunds initiated by the whitelisted sale contracts cause any amount of ether, up to the total deposited on behalf of the recipient, to be transferred directly from the vault to the depositing address. Refunds can only be initiated when the vault is in the Refunding
state.
State Transitions
As a Schedulable
contract, the ICOVault is in an "unactivated" state until the activation time, specified as seconds since the epoch, is reached (determined by the value now
within a transaction execution block). While unactivated, the activation and expiration times can be rescheduled if the ICO is delayed. Once the ICO has begun, determined by the execution block being at or beyond the activation time, the vault expiration cannot be delayed or altered.
The vault begins in an Active
state. The functions close()
and allowRefunds()
, callable by anyone, cause a permanent state transition to Closed
and Refunding
respectively. These functions will revert
unless the transaction block is executed at or after the vault's expiration time, and the amount of ether raised and held by the vault is appropriate for the requested state: at least as much as the goal value for close()
, less than the goal value for allowRefunds()
.
In other words, if the fundraising goal is not met, no member of the Vidy team is needed for the vault to enter a Refunding
state. This supports a trustless guarantee that refunds will be honored. Similarly, if the goal is reached, it is easy to transition it to a Closed
state and thus transfer the ether raised to the InvestorWallet
(provided as the wallet
address for the vault).
Whitelisting
The ICOVault
is deployed as a separate step from the migration of the sale contracts. When the sale contracts are deployed and their addresses known, those addresses are added to the vault's whitelist.
Internally, the ICOVault
uses NoRemovalWhitelist
as a supercontract; the sale contracts added to the whitelist cannot later be removed. This guarantees protection against a possible malicious action by the vault owner: removing a whitelisted sale to prevent it from issuing refunds. This action would not benefit the vault owner as it would not allow them to withdraw the ether themselves (Closed and Refunding states are still mutually exclusive, and state transition is unrelated to whitelisting), but it is prevented nonetheless.
Public interface
Construction
The ICOVault constructor requires a wallet address (to which ether will be transferred in the event of a successful ICO), a goal (soft cap), minimum and maximum goals, and an initial whitelist. When deployed to the Ethereum network, it will receive additional setup from the migration script, eventually settling with this configuration:
- The immutable wallet address, to which ether will be sent after a successful ICO. This will be the
InvestorWallet
contract. - A contract owner (the account deploying contracts to the Ethereum network).
- Activation and expiration times, mutable by the contract owner until the activation time is reached, then immutable.
- A current ether fundraising goal (specified in wei). The goal can be adjusted by the owner up to the vault's expiration time.
- Minimum and maximum values for the fundraising goal. Immutable bounds on any changes made to the vault's goal. We intend to adjust the goal only to respond to unanticipated changes in ether value; these immutable bounds trustlessly limit our ability to do so.
- In the
Active
state, of possible states{Active, Closed, Refunding}
.
After this configuration, the Ethereum address which deployed the ICOVault
contract retains ownership. This is used during later deployment of the sale contracts so they can be added to the vault's whitelist. The owner address, and the address to which ether is forwarded upon a successful ICO, are not the same; "ownership" in this case allows contract administration but not receipt of funds.
Events
event Closed()
: Emitted when the vault transitions to Closed
state.
event RefundsEnabled()
: Emitted when the vault transitions to Refunding
state.
event Withdrawn(uint256 amount)
: Emitted when ether is withdrawn from the vault into the wallet, including when the vault is Closed
but also if withdraw()
is called before the end of the sale (if the goal has been reached).
event GoalReached(uint256 amount)
: Emitted when a transaction causes the total deposited to exceed the goal (either because a deposit was made or the goal was lowered).
event Deposited(address indexed depositor, uint256 amount)
: Emitted when a deposit was made. depositor
is the address of the investor providing the funds, not the address which made the deposit function call. In our design, depositor
is the investor specified as an argument to deposit
, which is called by a sale contract. amount
is in wei.
event Refunded(address indexed depositor, address indexed beneficiary, uint256 amount)
: Emitted when a refund is made. depositor
is the original depositor of the funds; beneficiary
is who they were sent to. In our implementation the two will always be the same address. amount
is in wei.
event OwnershipRenounced(address indexed previousOwner)
: Emitted when the current owner renounces ownership and the contract transitions permanently into a non-owned state.
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
: Emitted when the current owner transfers ownership to another address.
event WhitelistedAddressAdded(address indexed addr)
: Emitted when an address is added to the internal whitelist (i.e. a sale contract is authorized to make deposits).
Fields and Constant Functions
mapping (address => uint256) public deposited
: Records the amount of ether, in wei, deposited by any given address. Adjusts when deposits are made or refunds issued.
uint256 public totalDeposited
: Records the total amount deposited across all investors. Adjusts when deposits are made or refunds issued.
uint256 public depositMinimum
: The minimum amount accepted as a deposit. If an attempt is made to deposit a total amount of ether lower than this value (in wei), the deposit will be refused. This calculation is made with respect to the resulting deposit total for the address, meaning a first-time deposit lower than this value will revert, but if an investor already has a nonzero amount deposited, a small additional deposit may be accepted.
uint256 public depositMaximum
: The maximum amount accepted as a deposit. If an attempt is made to deposit a total amount of ether greater than this value (in wei), the deposit will be refused. This calculation is made with respect to the resulting deposit total for the address, meaning an investor who has already deposited a significant amount will only be able to make additional deposits up to a combined total of this value.
uint256 public goal
: The current fundraising goal for the vault, in wei. Alterable by the contract owner
within the immutable minimumGoal
and maximumGoal
, and only until expirationTime
is reached.
uint256 public minimumGoal
: The lower bound for goal adjustment during the sale period (before expirationTime
is reached). Immutable.
uint256 public maximumGoal
: The upper bound for goal adjustment during the sale period (before expirationTime
is reached). Immutable.
address public wallet
: The address to which ether will be sent when the vault transitions to the Closed
state. Immutable.
State public state
: The current state of the vault: {Active, Closed, Refunding}
.
address public owner
: The address of the contract owner. Some functions will revert if msg.sender
is not this address.
uint256 public activationTime
: The time, in seconds since the epoch, after which changes to its expirationTime
are disallowed.
uint256 public expirationTime
: The time, in seconds since the epoch, at which the ICO will end.
mapping (address => bool) public whitelist
: Whether a given address appears on the whitelist; i.e. if that address is authorized to initiate deposits or refunds. Some functions will revent if whitelist[msg.sender]
is not true.
Non-constant Functions
function transferOwnership(address newOwner) public
: Set owner = newOwner
. Requirements: msg.sender == owner
.
function renounceOwnership() public
: Set owner = address(0)
. Requirements: msg.sender == owner
.
function addAddressToWhitelist(address _person) public
: Add the provided address to whitelist
, allowing that address to perform certain whitelist-only function calls. Requirements: msg.sender == owner
.
function addAddressesToWhitelist(address[] _people) public
: Add the provided addresses to whitelist
, allowing them to perform certain whitelist-only function calls. Requirements: msg.sender == owner
.
function removeAddressFromWhitelist(address _person) public
: Provided by the supercontract Whitelist
interface, but always reverts.
function removeAddressesFromWhitelist(address[] _people) public
: Provided by the supercontract Whitelist
interface, but always reverts.
function schedule(uint256 _activationTime, uint256 _expirationTime) public returns (bool success)
: Sets activationTime
and expirationTime
to the provided values. Requirements: msg.sender == owner
, and the execution block occurs before the previous value of activationTime
.
function activate() public returns (bool success)
: Sets activationTime
to the execution block timestamp. Requirements: msg.sender == owner
, and the execution block occurs before the previous value of activationTime
.
function cancel() public returns (bool success)
: Sets activationTime
and expirationTime
to 0. Requirements: msg.sender == owner
, and the execution block occurs before the previous value of activationTime
.
function deposit(address _investor) public payable
: Deposit the transferred ether into the vault on behalf of the indicated investor, updating deposited
and totalDeposited
. The ether received will be included in the lump transfer to the vault's wallet
when it transitions to a Closed
state, or made available as a refund to _investor
when it transitions to Refunding
. Requirements: msg.sender
is on the whitelist and the vault is in Active
state.
function refund(address _investor) public
: Transfer all ether deposited by _investor
back to their address, updating deposited
and totalDeposited
accordingly. Requirements: msg.sender
is on the whitelist and the vault is in Refunding
state.
function refund(address _investor, uint256 _amount) public
: Transfer _amount
ether deposited by _investor
back to their address, updating deposited
and totalDeposited
accordingly. Requirements: msg.sender
is on the whitelist, the vault is in Refunding
state, and deposited[_investor]
is at least _amount
.
function close() public
: Transition the vault to Closed
state, transferring its entire balance of stored ether to the wallet
address which was provided at construction. Requirements: the execution block timestamp at least expirationTime
, the vault is in Active
state, and totalDeposited
is at least the current goal
.
function allowRefunds() public
: Transition the vault to Refunded
state. Refunds are provided upon request through the refund
functions. Requirements: the execution block timestamp at least expirationTime
, the vault is in Active
state, and totalDeposited
is less than the current goal
.
function changeGoal(uint256 _newGoal) public
: Change the goal (soft cap) of the vault to the specified amount, represented in wei. A comparison between totalDeposited
and goal
determines the allowed state transition after the vault's expiration time. This function allows the Vidy team some flexibility to adjust the ICO soft cap to account for unanticipated fluctuations in ether value. Requirements: msg.sender == owner
, the execution block timestamp is less than expirationTime
, and the requested goal is within the range bounded by minimumGoal
and maximumGoal
.
function withdraw() public
: Transfer all accumulated ether in the vault to the wallet
address which was provided at construction. Requirements: the vault is in Active
state, and totalDeposited
is at least the current goal
.
Implementation Notes
The SafeMath library used for all arithmetic is drawn from the openzeppelin-solidity
suite: https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol. This library protects against integer overflow / underflow and in so doing prevents refund overdrafts.
ICOVault
is a descendent of Vault
, an abstract contract specifying the basic vault interface but leaving implementation to subcontracts. Different aspects of vault functionality: time and deposit-dependent state transitions, whitelisted access to deposit and refund functions, etc., are implemented in small, separate abstract subcontracts; the behavior is combined in a concrete contract using multiple inheritance.
When ICOVault
is deployed, a significant amount of funding will likely already have been raised from sources outside the blockchain. To account for this, the vault's goal
value will not be the full soft cap specified in the whitepaper; it will have been adjusted downward to represent the remaining amount needed to reach the fundraising goal.
The minimum and maximum deposit limits are applied per-address, so an investor depositing under multiple addresses could potentially exceed the maximum limit. The responsibility to prevent this is held off-blockchain, at the KYC verification stage; the sale contracts only allow contributions from whitelisted addresses, and each individual who passes KYC verification will only be able to register one single address to that whitelist.
Sales
Some early fundraising and VidyCoin distribution will occur outside of the blockchain. The final stages of the ICO, the whitelisted pre-ICO raise and the public ICO raise, are each managed trustlessly on the blockchain as sale contracts. The design and source code of each of the two sales is very similar, differing only in a few key details. Both sale contracts share the ICOVault
described above, both to store ether received from sales, and as the determiner of whether the ICO was successful (or refunds should be issued).
Each sale, pre-ICO and public ICO, is structured as a pair of contracts: one acting as the sale contract and one as a whitelist for that sale. The sale contracts use the ICOVault
to measure progress towards the ICO soft cap; the hard cap is represented implicitly as the total token inventory of the sale multiplied against the sale price (i.e., the sale contract can only raise as much ether as it has VidyCoins to distribute).
As described in the whitepaper, VidyCoins purchased from the pre-ICO sale are held in lock-up until after the completion of the ICO (the sale contract itself acts as the lock-up in this case). VidyCoins purchased from the public sale are received immediately.
For both sales, in the event of an unsuccessful fundraise, purchasers are required to surrender their purchased VidyCoins to receive their refund. For the pre-ICO this is easily done, since those VidyCoins will still be held by the sale contract in lock-up. The public sale provides refunds using the approve -> transferFrom
convention for ERC20 tokens. This refund function is available only to the original purchaser of the VidyCoins, and only up to the quantity purchased through the sale. I.e. the contract provides refunds with proof of sale and return of the tokens; it does not offer a buy back of VidyCoins presented from arbitrary addresses, or refunds to investors who have sold or otherwise distributed their VidyCoins.
Public Sale Contracts
The public ICO sale period is the final stage of the ICO; the expirationTime
of the ICOVault
corresponds to the end of the public sale period.
Although the public sale is open to anyone, the sale contract will only process purchases that originate from an address on an external whitelist. Customer addresses are added to this whitelist by our website upon successful completion of a third-party KYC flow: in other words, the whitelist represents members of the public whose proof of identity have been verified by KYC, with no other requirements.
The sale contract itself distributes an inventory of VidyCoins that is provided by the VidyTeamWallet (the initial owner of all VidyCoins). The specific number of VidyCoins to be provided to the sale contract is determined by the minimum allocation for the public raise, plus any unsold inventory from the pre-ICO period. At no point are these unsold VidyCoins held by an address directly controlled by a single individual: they are drained from the pre-ICO sale contract by the VidyTeamWallet (the only account able to do this), requiring multisignature verification, and then transferred to the public ICO contract with another multisignature verified transaction.
Because the public sale offering period is only one stage of the blockchain-managed fundraise, the public sale contract has its own activationTime
and expirationTime
. The public sale and ICOVault
share the same expirationTime
while the sale's activationTime
is after that of the vault.
Public Sale Whitelist
As mentioned, the public sale contract processes sales only to Ethereum addresses which appear on an external whitelist contract. This whitelist contract, PublicICOWhitelist
, is managed by our website and backend API and holds the addresses provided by potential investors who have completed third-party KYC verification.
The whitelist is only used to limit access to the sale contract's purchase function. Once VidyCoins are purchased by an address, removal of that address from the whitelist does not affect their ability to return those tokens later.
The whitelist contract itself shares an interface with the openzeppelin-solidity
whitelist implementation, although its internal structure is simpler. The contract is deployed separately so that its owner address (the backend API) can differ from that of the sale contract (owned by the VidyTeamWallet).
Public Sale Whitelist Interface
The PublicICOWhitelist
contract is empty when deployed, and owned by the website and backend API account. A third party KYC service SDK, integrated with our website, will provide verification that a potential investor has verified their identity. In response, the website will submit a blockchain transaction adding that customer's Ethereum address to the whitelist.
Construction
The whitelist is constructed and configured by the migration script such that it is empty of addresses. Ownership is transferred to the API backend. Addresses will be added as potential investors complete the KYC validation process hosted on our website.
Events
event WhitelistedAddressAdded(address indexed addr)
: Emitted when an address is added to the whitelist.
event WhitelistedAddressRemoved(address indexed addr)
: Emitted when an address is removed from the whitelist.
Fields and Constant Functions
mapping (address => bool) public whitelist
: Whether the given address appears on the whitelist.
Non-Constant Functions
function addAddressToWhitelist(address _person) public
: Add the provided address to the contract whitelist. Requirements: called by the contract owner.
function addAddressesToWhitelist(address[] _people) public
: Add the provided addresses to the contract whitelist. Requirements: called by the contract owner.
function removeAddressFromWhitelist(address _person) public
: Remove the provided address from the contract whitelist. Requirements: called by the contract owner.
function removeAddressesFromWhitelist(address[] _people) public
: Remove the provided addresses from the contract whitelist. Requirements: called by the contract owner.
Public Sale
The public sale contract PublicICOSale
holds a balance of VidyCoins, which it provides to investors upon purchase via an ether transfer. The sale supplies tokens according to a fixed exchange rate (price), which is the same for all customers. VidyCoins are transferred immediately upon purchase (within the same transaction) to the address which provided the ether. The total amount of VidyCoins distributable by the sale contract is determined by the contract's VIDY balance, which it receives via transfer from the VidyTeamWallet (the initial holder of all VidyCoins before distribution).
The sale contract keeps a record of the total amount of ether received from each investor address along with the number of VidyCoins purchased; if refunds are provided, a given investor can only receive refunds up to the amount received through the sale contract itself. In other words, if a single investor purchases VidyCoins from both the public sale and pre-ico sale, they would need to request refunds from each to receive their full investment amount.
Purchases from the sale contract are made automatically upon receipt of ether via the contract's fallback method. Returns follow the ERC20 token standard pattern of approve -> transferFrom
, meaning the investor needs to first invoke approve(..)
on the VidyCoin contract and then return(..)
on the sale, for the refund to be processed.
Public Sale Interface
PublicICOSale
, like ICOVault
, is Ownable
and Schedulable
. It also references an external whitelist contract, only allowing purchases from addresses that appear on that list. The VidyCoins it distributes are received via transfer from the VidyTeamWallet, which is also authorized to retrieve them (i.e., the VidyTeamWallet can cause the sale contract to transfer VidyCoins back to the wallet).
Construction
The PublicICOSale
constructor sets the token sale price, and addresses for the ERC20 token, vault, and whitelist contracts. These values are immutable. Price is expressed as a ratio, a conversion rate from token to ether, when both are expressed in the smallest possible subunit.
After construction, the deployment script continues configuration of the sale. The sale is scheduled, setting its activationTime
and expirationTime
according to the whitepaper terms (the expirationTime
will correspond to that set on the ICOVault
). Ownership of the sale is transferred to the VidyTeamWallet, and a transaction submitted to that wallet to transfer VidyCoins to the sale. Finally, the sale contract address is added to the ICOVault
whitelist, causing the vault to accept deposits sent by the sale contract (which are made on behalf of investors).
Like all Schedulable
contracts, the activationTime
and expirationTime
of the sale can be changed by the owner up to the moment the current activationTime
is reached, as determined by examining the execution block timestamp. Once activationTime
has been reached, the expirationTime
becomes immutable. In other words, once the public sale period begins, the time at which it ends cannot be changed; similarly, the public ICO sale period is necessarily bounded by activation and expiration times of the ICOVault
, since the vault must be in Active
state to receive deposits.
Events
event Sale(address indexed to, uint256 price, uint256 count)
: Emitted when a purchase of at least 1 subunit of VidyCoin is made. price
is the ether received, in wei; count
is the number of VidyCoins provided, in the smallest possible subunit. i.e. a price
of 1e18
is 1 ETH
; a count
of 1e18
is 1 VIDY
.
event Refund(address indexed to, uint256 price, uint256 count)
: Emitted when at least 1 wei worth of VidyCoin is returned. price
is the ether returned to the investor, in wei; count
is the number of VidyCoins returned, in the smallest possible subunit. i.e. a price
of 1e18
is 1 ETH
; a count
of 1e18
is 1 VIDY
.
event OwnershipRenounced(address indexed previousOwner)
: Emitted when the current owner renounces ownership and the contract transitions permanently into a non-owned state.
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
: Emitted when the current owner transfers ownership to another address.
Fields and Constant Functions
uint256 public priceNumerator
: The numerator of the ratio expressing a VidyCoin -> ether exchange rate. If the ratio is > 1, a VidyCoin is worth more than an ETH of ether.
uint256 public priceDenominator
: The denominator of the ratio expressing a VidyCoin -> ether exchange rate. If the ratio is < 1, a VidyCoin is worth less than an ETH of ether.
uint256 public amountRaised
: The total amount of ether, in wei, that has been raised by purchases from this sale. Does not reflect the total quantity raised across sales; i.e., will likely be less than ICOVault
's totalDeposited
amount.
mapping (address => uint256) public balance
: The amount of ether, in wei, received from any given investment address.
mapping (address => uint256) public sold
: The number of VidyCoins, in the smallest possible subunit, transferred to any given investment address.
address public owner
: The address of the contract owner. Some functions will revert if msg.sender
is not this address.
uint256 public activationTime
: The time, in seconds since the epoch, after which changes to its expirationTime
are disallowed and purchases become possible.
uint256 public expirationTime
: The time, in seconds since the epoch, at which the public sale ends; purchases will be disallowed.
address public token
: The ERC20 token being distributed by this sale: the address of the VidyCoin
contract.
address public vault
: The Vault
being used to store ether upon sale, and transfer ether upon returns: the address of the ICOVault
contract.
function stock() view public returns (uint256 _count)
: Returns the number of VidyCoins currently held by the sale contract; equal to the VidyCoin.balanceOf(..)
the sale contract.
function isGoalReached() view public returns (bool reached)
: Whether the ICO sales goal (soft cap) has been reached. Determined by examining the ICOVault
's totalDeposited
and goal
.
Non-Constant Functions
function () public payable
: Fallback function for receipt of ether. This is the purchase method: ether is transferred directly to the sale contract, which processes it as an attempted purchase. Only if the purchase results in the successful transfer of at least one subunit of VidyCoin to the purchaser is the ether accepted; otherwise, the transfer is reverted. The purchase is reflected in the sale's internal state (balance
, sold
, and amountRaised
) and the ether itself is forwarded in full to the ICOVault
at the address vault
. The VidyCoins purchased are transferred by the sale contract to the purchasing address. Requirements: msg.sender
is on the whitelist whose contract address was provided at construction, the ether received is sufficient to purchase a nonzero amount of VidyCoin, the VidyCoin balance of the sale contract is sufficient to transfer the appropriate amount to the purchaser, the execution block timestamp is between activationTime
and expirationTime
, the sale contract is on the ICOVault
whitelist, and the ICOVault
is in Active
state and successfully receives the ether deposit.
function refund(uint256 _count) public returns (bool success)
: Initiates a refund of _count
subunits of VidyCoin. The tokens are withdrawn from the caller's account using transferFrom
; for this to succeed, the caller must have previously approve
d a withdrawal by the sale contract address. The equivalent value of ether, according to the sale price exchange rate, is transferred directly from the ICOVault
to the investor's address (it does not pass through the sale contract). If _count
is less than the number of VidyCoins purchased by the investor, a partial, pro-rated refund is provided. Rounded errors may result in a few lost wei if a partial refund is provided, but when the full total of VidyCoins is returned, the entire existing ether sale balance is returned (correcting for any rounding errors of earlier partial returns). The return is reflected in updates to the sale's state (balance
, sold
, amountRaised
). Requirements: _count
subunits of VidyCoin are sufficient to receive at least 1 wei worth of ether, the caller has a VidyCoin balance of at least _count
and has approve
d a withdrawal of at least _count
by the sale contract, the caller purchased at least _count
worth of VidyCoin from this sale contract that has not previously been returned, the sale contract is on the ICOVault
whitelist, the ICOVault
is in Returning
state and successfully transfers ether to the caller's account as a return.
function provideRefund(address _to, uint256 _count) public
: Initiates a refund of _count
subunits of VidyCoin to _to
, as if requested by _to
with refund(_count)
, except that the transfer of VidyCoins from _to
to the sale contract will be omitted. Internal records still will be updated and ether returned from the vault. This function is a fallback for us to provide refunds to users who mistakenly transfer
ed their VidyCoins to the sale contract (verifiable by examining the VidyCoin event log) rather than followed the approve -> transferFrom
pattern; we intend for most, in not all, returns to be processed through the return(..)
function. Requirements: _count
subunits of VidyCoin are sufficient to receive at least 1 wei worth of ether, _to
purchased at least _count
worth of VidyCoin from this sale contract that has not previously been returned, the sale contract is on the ICOVault
whitelist, the ICOVault
is in Returning
state and successfully transfers ether to the caller's account as a return.
function drainProducts(address _to, uint256 _count) public
: Transfers _count
subunits of VidyCoin from the sale contract to the address _to
. Requirements: msg.sender == owner
, the sale contract has at least _count
subunits of VidyCoin.
function changeExternalWhitelist(address _address) public returns (bool success)
: Change the contract address referenced to determine if an address appears on the whitelist. The contract at _address
should implement the Whitelist
interface. Requirement: called by the contract's owner.
Implementation Notes
The SafeMath library used for all sale arithmetic is drawn from the openzeppelin-solidity
suite: https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol. This library protects against integer overflow / underflow and in so doing prevents refund overdrafts. The Conversion library used for exchanges between ether and token quantities also relies on SafeMath.
Token price is expressed as a ratio -- the conversion rate from tokens to ether -- when both are expressed in the smallest possible subunit. For VidyCoin, this unit is unnamed; for ether it is "wei". Because both ether and VidyCoin use the same decimal notation (18 decimal places below whole numbers), this is equivalent to an exchange rate from VIDY
to ETH
. For example, with a price of 1 / 40000
, 1 ETH
will purchase 40,000 VIDY
.
Pre-ICO Sale Contracts
The contracts used for the pre-ICO sale are extremely similar to those used in the public ICO, with only a few key differences. The whitelist contract has identical functionality to that used for the public sale, although the conditions for being whitelisted are different; the sale contract imposes a lock-up period before investors retrieve their VidyCoins rather than transferring them immediately.
The pre-ICO sale period is scheduled before the public ICO offering; the activationTime
of the ICOVault
corresponds to the start of the pre-ICO. The expirationTime
of the pre-ICO sale occurs before the public sale activates, staggering the two sales in time. The pre-ICO sale offers VidyCoins at a better exchange rate than the public ICO, but only to an approved list of investors. Compared to the public offering, there are more stringent requirements for approval than simple KYC verification. The specific conditions for approval are established outside the blockchain and not fully expressed in the whitepaper.
VidyCoins purchased from the pre-ICO sale contract are retained by the sale contract itself until the lock-up period ends, which will be scheduled to occur after the end of the ICO (the expirationTime
of ICOVault
). If the ICO is successful, purchasers can retrive their VidyCoins from the sale at any time after the lock-up period ends. If the ICO is unsuccessful, the sale contract will process returns for the VidyCoins still held in lock-up (because they have not been transferred to the investor, there is no need for the investor to transfer any back).
Pre-ICO Sale Whitelist Contract
The pre-ICO is a private sale, open only to a whitelist of approved investors. The conditions for an address appearing on the whitelist are determined outside the blockchain, not by contract behavior or KYC verification. The list of investor addresses will be added to the whitelist during deployment according to the migration script; any additional investors approved during the pre-ICO sale period can be added to the whitelist as needed.
The whitelist, PreICOWhitelist
, is only used to limit access to the sale contract's purchase function. Once VidyCoins are purchased by an address, removal of that address from the whitelist does not affect their ability to retrieve those tokens from the lock-up or initiate a refund if the ICO is unsuccessful.
Construction
The whitelist is constructed and configured by the migration script which iterates through a known list of approved investor addresses, adding them to the whitelist in batches. Once migration is complete the list is populated by the investor addresses who will make purchase during the pre-ICO. Contract ownership is retained by the deploying account in case additional investors must be manually added during the sale period.
Events
event WhitelistedAddressAdded(address indexed addr)
: Emitted when an address is added to the whitelist.
event WhitelistedAddressRemoved(address indexed addr)
: Emitted when an address is removed from the whitelist.
Fields and Constant Functions
mapping (address => bool) public whitelist
: Whether the given address appears on the whitelist.
Non-Constant Functions
function addAddressToWhitelist(address _person) public
: Add the provided address to the contract whitelist. Requirements: called by the contract owner.
function addAddressesToWhitelist(address[] _people) public
: Add the provided addresses to the contract whitelist. Requirements: called by the contract owner.
function removeAddressFromWhitelist(address _person) public
: Remove the provided address from the contract whitelist. Requirements: called by the contract owner.
function removeAddressesFromWhitelist(address[] _people) public
: Remove the provided addresses from the contract whitelist. Requirements: called by the contract owner.
Pre-ICO Sale
The private sale contract PreICOSale
holds a balance of VidyCoins, which it offers for sale to investors upon purchase via an ether transfer. The sale offers tokens according to a fixed exchange rate (price), which is the same for all customers of the sale. Upon purchase, the appropriate number of VidyCoins -- still held by the PreICOSale
contract -- are recorded in a lock-up ledger within the sale contract itself. They are no longer reflected in the available stock
and can neither be purchased by other investors, nor withdrawn by the contract owner. The transfer of those VidyCoins to the investor, assuming a successful ICO, is trustlessly guaranteed.
The sale contract keeps a record of the total amount of ether received from each investor address along with the number of VidyCoins purchased; if refunds are provided, a given investor can only receive refunds up to the amount received through the sale contract itself. In other words, if a single investor purchases VidyCoins from both the public sale and pre-ICO sale, they would need to request refunds from each to receive their full investment amount.
Purchases from the sale contract are made automatically upon receipt of ether via the contract's fallback method. The VidyCoins purchased are retained by the sale contract in lock-up, allowing later retrieval by the purchaser once the lock-up delay has been exceeded. If the ICO should fail to reach its goal (soft cap), investors can receive ether refunds for all unretrieved VidyCoins still in lock-up, and/or refund retrieved VidyCoins using the standard approve
/ transferFrom
usage pattern. As a fallback for users who accidentally transfer
their retrieved tokens back to the sale contract, the Vidy team can manually initiate a refund on that user's behalf (after verifying that their VidyCoins have been returned).
Pre-ICO Sale Interface
PreICOSale
, like ICOVault
, is Ownable
and Schedulable
. It also references an external whitelist contract, only allowing purchases from addresses that appear on that list. The VidyCoins it offers for sale are received via transfer from the VidyTeamWallet, which is also authorized to retrieve them (i.e., the VidyTeamWallet can cause the sale contract to transfer VidyCoins back to the wallet). VidyCoins that are being kept in lock-up cannot be retrieved by the VidyTeamWallet unless the ICO has failed and refunds are being issued.
Under normal circumstances, VidyCoin purchases are a two-step process. An investor makes a purchase by transferring ether to the sale contract; the contract records the appropriate quantity of VidyCoins in its lock-up. When the lock-up period ends, the investor retrieves their VidyCoins from the sale contract, causing an ERC20 transfer
into their account. If the ICO is unsuccessful, investors can request a return of their invested ether, surrendering their purchased VidyCoins either from the lock-up or from their own wallet balance.
A few additional functions are available to the sale contract owner
(the VidyTeamWallet) in case they are needed: lock-ups can be released early if the ICO is successful, or invalidated if the ICO ended without reaching its soft cap. Invalidating the lock-up allows the full balance of VidyCoins held by the sale to be withdrawn by the VidyTeamWallet, but since this is only possible if the ICO ends unsuccessfully, it does not affect the ability of investors to withdraw their purchased VidyCoins after a successful ICO.
Construction
The PreICOSale
constructor sets the token sale price, a time for the token lock-up to release, and addresses for the ERC20 token, vault, and whitelist contracts. These values are immutable. Price is expressed as a ratio, a conversion rate from token to ether, when both are expressed in the smallest possible subunit.
After construction, the deployment script continues configuration of the sale. The sale is scheduled, setting its activationTime
and expirationTime
according to the whitepaper terms (the activationTime
will correspond to that set on the ICOVault
). Ownership of the sale is transferred to the VidyTeamWallet, and a transaction submitted to that wallet to transfer VidyCoins to the sale. Finally, the sale contract address is added to the ICOVault
whitelist, causing the vault to accept deposits sent by the sale contract (which are made on behalf of investors).
Like all Schedulable
contracts, the activationTime
and expirationTime
of the sale can be changed by the owner up to the moment the current activationTime
is reached, as determined by examining the execution block timestamp. Once activationTime
has been reached, the expirationTime
becomes immutable. In other words, once the public sale period begins, the time at which it ends cannot be changed; similarly, the pre-ICO sale period is necessarily bounded by activation and expiration times of the ICOVault
, since the vault must be in Active
state to receive deposits.
Events
event Sale(address indexed to, uint256 price, uint256 count)
: Emitted when a purchase of at least 1 subunit of VidyCoin is made. price
is the ether received, in wei; count
is the number of VidyCoins provided, in the smallest possible subunit. i.e. a price
of 1e18
is 1 ETH
; a count
of 1e18
is 1 VIDY
.
event Refund(address indexed to, uint256 price, uint256 count)
: Emitted when at least 1 wei worth of VidyCoin is returned. price
is the ether returned to the investor, in wei; count
is the number of VidyCoins returned, in the smallest possible subunit. i.e. a price
of 1e18
is 1 ETH
; a count
of 1e18
is 1 VIDY
.
event OwnershipRenounced(address indexed previousOwner)
: Emitted when the current owner renounces ownership and the contract transitions permanently into a non-owned state.
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
: Emitted when the current owner transfers ownership to another address.
Fields and Constant Functions
uint256 public priceNumerator
: The numerator of the ratio expressing a VidyCoin -> ether exchange rate. If the ratio is > 1, a VidyCoin is worth more than an ETH of ether.
uint256 public priceDenominator
: The denominator of the ratio expressing a VidyCoin -> ether exchange rate. If the ratio is < 1, a VidyCoin is worth less than an ETH of ether.
uint256 public amountRaised
: The total amount of ether, in wei, that has been raised by purchases from this sale. Does not reflect the total quantity raised across sales; i.e., will likely be less than ICOVault
's totalDeposited
amount.
mapping (address => uint256) public balance
: The amount of ether, in wei, received from any given investment address.
mapping (address => uint256) public sold
: The number of VidyCoins, in the smallest possible subunit, transferred to any given investment address.
address public owner
: The address of the contract owner. Some functions will revert if msg.sender
is not this address.
uint256 public activationTime
: The time, in seconds since the epoch, after which changes to its expirationTime
are disallowed and purchases become possible.
uint256 public expirationTime
: The time, in seconds since the epoch, at which the public sale ends; purchases will be disallowed.
address public token
: The ERC20 token being distributed by this sale: the address of the VidyCoin
contract.
uint256 public lockupReleaseTime
: The time, in seconds passed the epoch, at which VidyCoins held in lock-up are released to investors. Will be set to occur after the end of the ICO.
uint256 public lockupTokenTotal
: The total quantity of VidyCoins, in the smallest possible subunit, held in lock-up: the amount purchased by investors, but not yet withdrawn or refunded.
mapping (address => uint256) public lockupTokenBalance
: The number of VidyCoins, in the smallest possible subunit, held in lock-up for any given investment address (and not yet withdrawn or refunded).
bool public lockupReleasedEarly
: Whether the contract owner has released VidyCoins in lock-up for retrieval by their purchasers. If released, they may be withdrawn immediately upon successful completion of the ICO, without waiting the additional time until lockupReleaseTime
. The contract owner can only release VidyCoins from lock-up after the successful end of the ICO.
bool public lockupInvalidated
: Whether the contract owner has invalidated the lock-up so that all VidyCoins held there can be drained from the sale contract. Lock-up invalidation does not affect the records of how many VidyCoins were purchased and held there, so refunds will still be honored by the contract. The contract owner can only invalidate the lock-up after the sale ends unsuccessfully and the lockup period has elapsed.
address public vault
: The Vault
being used to store ether upon sale, and transfer ether upon returns: the address of the ICOVault
contract.
function stock() view public returns (uint256 _count)
: Returns the number of VidyCoins currently held by the sale contract, excluding those in lock-up; equal to the VidyCoin.balanceOf(..)
the sale contract minus lockupTokenTotal
. If lockupInvalidated
, then equal to the VidyCoin.balanceOf(..)
the sale contract.
function isGoalReached() view public returns (bool reached)
: Whether the ICO sales goal (soft cap) has been reached. Determined by examining the ICOVault
's totalDeposited
and goal
.
Non-Constant Functions
function () public payable
: Fallback function for receipt of ether. This is the purchase method: ether is transferred directly to the sale contract, which processes it as an attempted purchase. The ether received must be sufficient to successfully purchase at least 1 subunit of VidyCoin; otherwise, the transfer is reverted. The purchase is reflected in the sale's internal state (balance
, sold
, and amountRaised
) and the ether itself is forwarded in full to the ICOVault
at the address vault
. The amount of VidyCoins purchase is recorded in the lock-up record (lockupTokenBalance
and lockupTokenTotal
); the tokens themselves are retained in the sale contract's account until later retrieval. Requirements: msg.sender
is on the whitelist whose contract address was provided at construction, the ether received is sufficient to purchase a nonzero amount of VidyCoin, the VidyCoin balance of the sale contract excluding the amount in lock-up is sufficient to transfer the appropriate amount to the purchaser, the execution block timestamp is between activationTime
and expirationTime
, the sale contract is on the ICOVault
whitelist, and the ICOVault
is in Active
state and successfully receives the ether deposit.
function refundLockUp() public returns (bool success)
: Initiates a refund of all ether sent by the caller to this sale contract. The ether is transferred directly from the ICOVault
to the investor's address (it does not pass through the sale contract). The return is reflected in updates to the sale's state (balance
, sold
, amountRaised
, lockupTokenBalance
, and lockupTokenTotal
); in particular, the caller's lockup balance is reduced to 0. There is no transfer of VidyCoins from the caller's balance to the sale contract; the refund is processed as a release of the VidyCoins already in the contract's lock-up. Requirements: the sale contract is on the ICOVault
whitelist, the ICOVault
is in Returning
state and successfully transfers ether to the caller's account as a return.
function retrieve() public returns (bool success)
: Causes the sale contracts to transfer
the amount of VidyCoins held in lock-up for the caller -- lockupTokenBalance[msg.sender]
-- to the caller's address. The retrieval is reflected in the sale's lockup state (lockupTokenBalance
, and lockupTokenTotal
). Requirements: the lock-up period has ended or the contract owner has manually released the lock-up.
function refund(uint256 _count) public returns (bool success)
: Initiates a refund of _count
subunits of VidyCoin. The tokens are withdrawn from the caller's account using transferFrom
; for this to succeed, the caller must have previously approve
d a withdrawal by the sale contract address. The equivalent value of ether, according to the sale price exchange rate, is transferred directly from the ICOVault
to the investor's address (it does not pass through the sale contract). If _count
is less than the number of VidyCoins purchased by the investor, a partial, pro-rated refund is provided. Rounded errors may result in a few lost wei if a partial refund is provided, but when the full total of VidyCoins is returned, the entire existing ether sale balance is returned (correcting for any rounding errors of earlier partial returns). The return is reflected in updates to the sale's state (balance
, sold
, amountRaised
). Requirements: _count
subunits of VidyCoin are sufficient to receive at least 1 wei worth of ether, the caller has a VidyCoin balance of at least _count
and has approve
d a withdrawal of at least _count
by the sale contract, the caller purchased at least _count
worth of VidyCoin from this sale contract that has not previously been returned, the sale contract is on the ICOVault
whitelist, the ICOVault
is in Returning
state and successfully transfers ether to the caller's account as a return.
function provideRefund(address _to, uint256 _count) public
: Initiates a refund of _count
subunits of VidyCoin to _to
, as if requested by _to
with refund(_count)
, except that the transfer of VidyCoins from _to
to the sale contract will be omitted. Internal records still will be updated and ether returned from the vault. This function is a fallback for us to provide refunds to users who mistakenly transfer
ed their VidyCoins to the sale contract (verifiable by examining the VidyCoin event log) rather than followed the approve -> transferFrom
pattern; we intend for most, in not all, returns to be processed through the return(..)
function. Requirements: _count
subunits of VidyCoin are sufficient to receive at least 1 wei worth of ether, _to
purchased at least _count
worth of VidyCoin from this sale contract that has not previously been returned, the sale contract is on the ICOVault
whitelist, the ICOVault
is in Returning
state and successfully transfers ether to the caller's account as a return.
function releaseLockUp() public returns (bool success)
: Allows VidyCoins held in lock-up to be retrieved by their purchasers before the lock-up period has ended. The remaining conditions for retrieval still apply: the ICOVault
reports that its goal (the soft cap) has been reached, and the execution block's timestamp occurs after expirationTime
. Requirements: called by the sale contract's owner.
function invalidateLockUp() public returns (bool success)
: Invalidates the lock-up of all VidyCoins sold, although the records (lockupTokenBalance
and lockupTokenTotal
) are unaffected. This allows the owner to withdraw the full amount of VidyCoins held by the sale contract, including those previously in lock-up. This can only be done under conditions that necessarily imply an unsuccessful ICO, which are mutually exclusive with investors retrieving their tokens from lock-up. Refunds will still be honored whether the lock-up is invalidated or not. Requirements: called by the sale contract's owner, the execution block's timestamp occurs after lockupReleaseTime
, the ICOVault
is in Returning
state.
function drainProducts(address _to, uint256 _count) public
: Transfers _count
subunits of VidyCoin from the sale contract to the address _to
. Requirements: msg.sender == owner
, the sale contract has at least _count
subunits of VidyCoin that are not currently in lock-up (or the lock-up has been invalidated).
Implementation Notes
The SafeMath library used for all sale arithmetic is drawn from the openzeppelin-solidity
suite: https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol. This library protects against integer overflow / underflow and in so doing prevents refund overdrafts. The Conversion library used for exchanges between ether and token quantities also relies on SafeMath.
Token price is expressed as a ratio -- the conversion rate from tokens to ether -- when both are expressed in the smallest possible subunit. For VidyCoin, this unit is unnamed; for ether it is "wei". Because both ether and VidyCoin use the same decimal notation (18 decimal places below whole numbers), this is equivalent to an exchange rate from VIDY
to ETH
. For example, with a price of 1 / 40000
, 1 ETH
will purchase 40,000 VIDY
.