-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal for simple exit contract #362
Changes from 3 commits
91ab01f
b205aa9
6c644f5
e5f5004
0edc4f2
cd8501b
0be26f9
6c1639a
88c6850
2817b08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,281 @@ | ||||||
pragma solidity 0.4.26; | ||||||
|
||||||
import "../../Math.sol"; | ||||||
import "../../Universe.sol"; | ||||||
import "../../Agreement.sol"; | ||||||
import "../../Reclaimable.sol"; | ||||||
|
||||||
import "../IEquityTokenController.sol"; | ||||||
import "../IEquityToken.sol"; | ||||||
import "../../ETO/IETOCommitment.sol"; | ||||||
import "../../Standards/IContractId.sol"; | ||||||
|
||||||
/* | ||||||
To test: | ||||||
* Disbursal by nominee | ||||||
* | ||||||
*/ | ||||||
|
||||||
contract ExitController is | ||||||
KnownInterfaces, | ||||||
Reclaimable, | ||||||
Agreement | ||||||
{ | ||||||
|
||||||
//////////////////////// | ||||||
// Events | ||||||
//////////////////////// | ||||||
|
||||||
/// log state transitions | ||||||
event LogStateTransition( | ||||||
uint32 oldState, | ||||||
uint32 newState, | ||||||
uint32 timestamp | ||||||
); | ||||||
|
||||||
event LogProceedsPayed( | ||||||
address investor, | ||||||
uint256 amountEquityTokens, | ||||||
uint256 amountPayed | ||||||
); | ||||||
|
||||||
event LogProceedsManuallyResolved( | ||||||
address lostAddress, | ||||||
address newAddress, | ||||||
uint256 amountEquityTokens, | ||||||
uint256 amountPayed | ||||||
); | ||||||
|
||||||
//////////////////////// | ||||||
// Types | ||||||
//////////////////////// | ||||||
|
||||||
// defines state machine of the exit controller | ||||||
enum State { | ||||||
Setup, // Initial state | ||||||
Payout, // Users can claim eur-t for tokens | ||||||
ManualPayoutResolution // Nominee can manually resolve payouts, user initiated payout is disabled | ||||||
} | ||||||
|
||||||
//////////////////////// | ||||||
// Immutable state | ||||||
//////////////////////// | ||||||
|
||||||
// a root of trust contract | ||||||
Universe private UNIVERSE; | ||||||
IERC223Token private EURO_TOKEN; | ||||||
// equity token from ETO | ||||||
IEquityToken private EQUITY_TOKEN; | ||||||
|
||||||
//////////////////////// | ||||||
// Mutable state | ||||||
//////////////////////// | ||||||
|
||||||
// controller lifecycle state | ||||||
State private _state; | ||||||
|
||||||
// exit values get set when exit proceedings start | ||||||
uint256 private _exitEquityTokenSupply = 0; | ||||||
uint256 private _exitAquisitionPriceEurUlps = 0; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a typo: |
||||||
uint256 private _manualPayoutResolutionStart = 0; | ||||||
|
||||||
// keep record of manually resolved payout | ||||||
mapping(address => bool) private payoutManuallyResolved; | ||||||
|
||||||
//////////////////////// | ||||||
// Modifiers | ||||||
//////////////////////// | ||||||
|
||||||
//////////////////////// | ||||||
// Constructor | ||||||
//////////////////////// | ||||||
|
||||||
constructor( | ||||||
Universe universe, | ||||||
IEquityToken equityToken | ||||||
) | ||||||
public | ||||||
Agreement(universe.accessPolicy(), universe.forkArbiter()) | ||||||
Reclaimable() | ||||||
{ | ||||||
UNIVERSE = universe; | ||||||
EURO_TOKEN = UNIVERSE.euroToken(); | ||||||
EQUITY_TOKEN = equityToken; | ||||||
_state = State.Setup; | ||||||
} | ||||||
|
||||||
sh-rp marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
//////////////////////// | ||||||
// External functions | ||||||
//////////////////////// | ||||||
|
||||||
// | ||||||
// Implements IControllerGovernance | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is that comment in place here? I'm wondering where |
||||||
// | ||||||
function state() | ||||||
public | ||||||
constant | ||||||
returns (State) | ||||||
{ | ||||||
return _state; | ||||||
} | ||||||
|
||||||
function payoutInfo() | ||||||
public | ||||||
constant | ||||||
returns (uint256 exitEquityTokenSupply, uint256 exitAquisitionPriceEurUlps, uint256 manualPayoutResolutionStart) | ||||||
{ | ||||||
return ( | ||||||
_exitEquityTokenSupply, _exitAquisitionPriceEurUlps, _manualPayoutResolutionStart | ||||||
); | ||||||
} | ||||||
|
||||||
// calculate how many eurotokens one would receive for the given amount of tokens | ||||||
function eligibleProceedsForTokens(uint256 amountTokens) | ||||||
public | ||||||
constant | ||||||
returns (uint256) | ||||||
{ | ||||||
if (_state == State.Setup ) { | ||||||
return 0; | ||||||
} | ||||||
// calculate the amount of eligible proceeds based on the total equity token supply and the | ||||||
// acquisition price | ||||||
return Math.mul(_exitAquisitionPriceEurUlps, amountTokens) / _exitEquityTokenSupply; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sadly I don't know about high precision math but there is also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The div round does a round up from .5 upwards, and I thought it would be safer to always round down the way / does. Or what do you think? |
||||||
} | ||||||
|
||||||
// calculate how many eurotokens the user with the given address would receive | ||||||
function eligibleProceedsForInvestor(address investor) | ||||||
public | ||||||
constant | ||||||
returns (uint256 equityTokens, uint256 proceeds) | ||||||
{ | ||||||
equityTokens = 0; | ||||||
proceeds = 0; | ||||||
|
||||||
if (payoutManuallyResolved[investor]) { | ||||||
return; | ||||||
} | ||||||
|
||||||
if (_state == State.Payout) { | ||||||
equityTokens = EQUITY_TOKEN.balanceOf(investor); | ||||||
} | ||||||
else if (_state == State.ManualPayoutResolution) { | ||||||
equityTokens = EQUITY_TOKEN.balanceOfAt(investor, _manualPayoutResolutionStart); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can add support for users without claimed tokens here by interacting with the
|
||||||
} | ||||||
proceeds = eligibleProceedsForTokens(equityTokens); | ||||||
return (equityTokens, proceeds); | ||||||
} | ||||||
|
||||||
// | ||||||
// IERC223TokenCallback (exit proceeds disbursal) | ||||||
// | ||||||
|
||||||
/// allows contract to receive and distribute proceeds | ||||||
/// this can only be done in the funded state | ||||||
function tokenFallback(address from, uint256 amount, bytes) | ||||||
public | ||||||
{ | ||||||
require(amount > 0, "NF_ZERO_TOKENS"); | ||||||
|
||||||
// if we're in the setup state, this contract is waiting | ||||||
// for the nominee to send the exit funds | ||||||
if (_state == State.Setup) { | ||||||
// we only allow eurotokens for this operation | ||||||
require(msg.sender == address(EURO_TOKEN), "NF_ETO_INCORRECT_TOKEN"); | ||||||
// only the nominee may send proceeds to this contract | ||||||
require(from == EQUITY_TOKEN.nominee(), "NF_ONLY_NOMINEE"); | ||||||
// start the payout | ||||||
startPayout(); | ||||||
} | ||||||
// when we already are in the closing state, investors can send | ||||||
// their tokens to be burned and converted to euro token | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
else if ( _state == State.Payout ) { | ||||||
// now we only allow conversion of the tokens into neumarks | ||||||
require(msg.sender == address(EQUITY_TOKEN), "NF_ETO_UNK_TOKEN"); | ||||||
// investor must have sent all of his tokens | ||||||
require(EQUITY_TOKEN.balanceOf(from) == 0, "NF_MUST_SEND_ALL_TOKENS"); | ||||||
// payout exit proceeds | ||||||
payExitProceeds(from, amount); | ||||||
} else { | ||||||
revert("UNEXPECTED_OPERATION"); | ||||||
} | ||||||
} | ||||||
|
||||||
function startManualPayoutResolution() | ||||||
public | ||||||
{ | ||||||
require(_state == State.Payout, "NF_INCORRECT_STATE"); | ||||||
require(msg.sender == EQUITY_TOKEN.nominee(), "NF_ONLY_NOMINEE"); | ||||||
transitionTo(State.ManualPayoutResolution); | ||||||
_manualPayoutResolutionStart = EQUITY_TOKEN.currentSnapshotId(); | ||||||
} | ||||||
|
||||||
function payoutManually(address lostWallet, address newWallet) | ||||||
public | ||||||
{ | ||||||
// only in manual payout state | ||||||
require(_state == State.ManualPayoutResolution, "NF_INCORRECT_STATE"); | ||||||
// only the nominee may do manual payouts | ||||||
require(msg.sender == EQUITY_TOKEN.nominee(), "NF_ONLY_NOMINEE"); | ||||||
// we need a valid receiver address | ||||||
require(newWallet != 0x0, "NF_INVALID_NEW_WALLET"); | ||||||
// we can only process wallets that have not been manually resolved yet | ||||||
require(payoutManuallyResolved[lostWallet] == false, "NF_ALREADY_PAYED_OUT"); | ||||||
// only allow when timestamp has moved to next date | ||||||
require(_manualPayoutResolutionStart < EQUITY_TOKEN.currentSnapshotId(), "NF_WAIT"); | ||||||
|
||||||
(uint256 _tokens, uint256 _proceeds) = eligibleProceedsForInvestor(lostWallet); | ||||||
require(_proceeds > 0, "NF_NO_PROCEEDS"); | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it makes sense to check claims of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say this is already done in the euro token controller contract in function isTransferAllowedPrivate(address from, address to, bool allowPeerTransfers). So wallets that are not verified can't receive our-t. Could you double check if you think this is the case too? |
||||||
payoutManuallyResolved[lostWallet] = true; | ||||||
EURO_TOKEN.transfer(newWallet, _proceeds, ""); | ||||||
emit LogProceedsManuallyResolved(lostWallet, newWallet, _tokens, _proceeds); | ||||||
} | ||||||
|
||||||
|
||||||
// | ||||||
// Implements IContractId (neufund-platform:ExitController) | ||||||
// | ||||||
|
||||||
function contractId() public pure returns (bytes32 id, uint256 version) { | ||||||
return (0x2d1ac7522107965d7626cc53b73381123e95c12589b64ae4bc7fac5015dc964b, 1); | ||||||
} | ||||||
|
||||||
//////////////////////// | ||||||
// Internal functions | ||||||
//////////////////////// | ||||||
|
||||||
function transitionTo(State newState) | ||||||
internal | ||||||
{ | ||||||
emit LogStateTransition(uint32(_state), uint32(newState), uint32(block.timestamp)); | ||||||
_state = newState; | ||||||
} | ||||||
|
||||||
//////////////////////// | ||||||
// Private functions | ||||||
//////////////////////// | ||||||
|
||||||
// start the exit when nominee sends exit funds | ||||||
function startPayout() | ||||||
private | ||||||
{ | ||||||
// get total number of equity tokens | ||||||
_exitEquityTokenSupply = EQUITY_TOKEN.totalSupply(); | ||||||
// get the total exit amount in eur-t for the given euqity tokens | ||||||
_exitAquisitionPriceEurUlps = EURO_TOKEN.balanceOf(this); | ||||||
// mark the company as closing, in our case this means "exiting" | ||||||
transitionTo(State.Payout); | ||||||
} | ||||||
|
||||||
// pay exit proceeds to an individual user | ||||||
function payExitProceeds(address investor, uint256 equityTokenAmount) | ||||||
private | ||||||
{ | ||||||
// payout euro tokens to investor | ||||||
uint256 _eligibleProceeds = eligibleProceedsForTokens(equityTokenAmount); | ||||||
EURO_TOKEN.transfer(investor, _eligibleProceeds, ""); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case transfer failed (for .e.g someone reclaimed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the same applies to manual payout too |
||||||
emit LogProceedsPayed(investor, equityTokenAmount, _eligibleProceeds); | ||||||
} | ||||||
|
||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -100,5 +100,8 @@ contract KnownInterfaces { | |||||
// Voting Center keccak256("IVotingCenter") | ||||||
bytes4 internal constant KNOWN_INTERFACE_VOTING_CENTER = 0xff5dbb18; | ||||||
|
||||||
// Voting Center keccak256("ExitController") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
bytes4 internal constant KNOWN_INTERFACE_EXIT_CONTROLLER = 0xca32f084; | ||||||
|
||||||
constructor() internal {} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's disallow reclaiming
EQUITY_TOKEN
completely andEURO_TOKEN
up until we reach eitherManualPayoutResolution
or a newClosed
state