Skip to content
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

Merged
merged 10 commits into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ tests:
EQUITY_TOKEN_DECIMALS=0 yarn truffle test test/ETO/* test/Company/* test/setup.js --network inprocess_test
EQUITY_TOKEN_DECIMALS=10 yarn truffle test test/ETO/* test/Company/* test/setup.js --network inprocess_test

test-single:
yarn truffle test $(test) test/setup.js --network inprocess_test --migrations_directory migrations_null

test-invest-into-eto-script: container
docker run --detach -it -p 8545:8545 --name platform-contracts --rm neufund/platform-contracts yarn testrpc
sleep 5
Expand Down
281 changes: 281 additions & 0 deletions contracts/Company/Extras/ExitController.sol
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,
Copy link

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 and EURO_TOKEN up until we reach either ManualPayoutResolution or a new Closed state

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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a typo: Acquisition

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that comment in place here? I'm wondering where state function implements IControllerGovernance

//
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;
Copy link
Contributor

@banciur banciur Mar 24, 2021

Choose a reason for hiding this comment

The 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 Math.divRound maybe it would be better her than simple /

Copy link
Author

Choose a reason for hiding this comment

The 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);
Copy link

Choose a reason for hiding this comment

The 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 ETOCommitment smart-contract.
Pseudo-code

  if (equityTokens == 0) {
                (claimedOrRefunded, equityTOkenAmount) = ETO_COMMITMENT.investorTicket(investor)

                if (claimedOrRefund == false) {
                    equityTokens = equityTOkenAmount;
                }
            }
           

}
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// their tokens to be burned and converted to euro token
// their tokens to be burned and converted to euro token

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");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes sense to check claims of newWallet and add requirement for KYC and bank account? It will help mitigate manual mistakes from nominee that will send those transactions.

Copy link
Author

Choose a reason for hiding this comment

The 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, "");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case transfer failed (for .e.g someone reclaimed EURO_TOKEN) we would still log event. Maybe we should wrap transfer in require?

Copy link

Choose a reason for hiding this comment

The 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);
}

}
3 changes: 3 additions & 0 deletions contracts/KnownInterfaces.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,8 @@ contract KnownInterfaces {
// Voting Center keccak256("IVotingCenter")
bytes4 internal constant KNOWN_INTERFACE_VOTING_CENTER = 0xff5dbb18;

// Voting Center keccak256("ExitController")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Voting Center keccak256("ExitController")
// Exit Controller keccak256("ExitController")

bytes4 internal constant KNOWN_INTERFACE_EXIT_CONTROLLER = 0xca32f084;

constructor() internal {}
}
Loading