liquid-funding-console/contracts/LiquidPledgingBase.sol

677 lines
24 KiB
Solidity

pragma solidity ^0.4.18;
/*
Copyright 2017, Jordi Baylina
Contributors: Adrià Massanet <adria@codecontext.io>, RJ Ewing, Griff
Green, Arthur Lunn
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./LiquidPledgingStorage.sol";
import "./PledgeAdmins.sol";
import "./Pledges.sol";
import "@aragon/os/contracts/apps/AragonApp.sol";
/// @dev `LiquidPledgingBase` is the base level contract used to carry out
/// liquidPledging's most basic functions, mostly handling and searching the
/// data structures
contract LiquidPledgingBase is AragonApp, LiquidPledgingStorage, PledgeAdmins, Pledges {
event Transfer(uint indexed from, uint indexed to, uint amount);
event CancelProject(uint indexed idProject);
/////////////
// Modifiers
/////////////
/// @dev The `vault`is the only addresses that can call a function with this
/// modifier
modifier onlyVault() {
require(msg.sender == address(vault));
_;
}
///////////////
// Constructor
///////////////
/// @param _vault The vault where the ETH backing the pledges is stored
function initialize(address _vault) onlyInit public {
require(_vault != 0x0);
initialized();
vault = ILPVault(_vault);
admins.length = 1; // we reserve the 0 admin
pledges.length = 1; // we reserve the 0 pledge
}
/////////////////////////////
// Public constant functions
/////////////////////////////
/// @notice Getter to find Delegate w/ the Pledge ID & the Delegate index
/// @param idPledge The id number representing the pledge being queried
/// @param idxDelegate The index number for the delegate in this Pledge
function getPledgeDelegate(uint64 idPledge, uint64 idxDelegate) external view returns(
uint64 idDelegate,
address addr,
string name
) {
Pledge storage p = _findPledge(idPledge);
idDelegate = p.delegationChain[idxDelegate - 1];
PledgeAdmin storage delegate = _findAdmin(idDelegate);
addr = delegate.addr;
name = delegate.name;
}
///////////////////
// Public functions
///////////////////
/// @notice Only affects pledges with the Pledged PledgeState for 2 things:
/// #1: Checks if the pledge should be committed. This means that
/// if the pledge has an intendedProject and it is past the
/// commitTime, it changes the owner to be the proposed project
/// (The UI will have to read the commit time and manually do what
/// this function does to the pledge for the end user
/// at the expiration of the commitTime)
///
/// #2: Checks to make sure that if there has been a cancellation in the
/// chain of projects, the pledge's owner has been changed
/// appropriately.
///
/// This function can be called by anybody at anytime on any pledge.
/// In general it can be called to force the calls of the affected
/// plugins, which also need to be predicted by the UI
/// @param idPledge This is the id of the pledge that will be normalized
/// @return The normalized Pledge!
function normalizePledge(uint64 idPledge) public returns(uint64) {
Pledge storage p = _findPledge(idPledge);
// Check to make sure this pledge hasn't already been used
// or is in the process of being used
if (p.pledgeState != PledgeState.Pledged) {
return idPledge;
}
// First send to a project if it's proposed and committed
if ((p.intendedProject > 0) && ( _getTime() > p.commitTime)) {
uint64 oldPledge = _findOrCreatePledge(
p.owner,
p.delegationChain,
0,
0,
p.oldPledge,
p.token,
PledgeState.Pledged
);
uint64 toPledge = _findOrCreatePledge(
p.intendedProject,
new uint64[](0),
0,
0,
oldPledge,
p.token,
PledgeState.Pledged
);
_doTransfer(idPledge, toPledge, p.amount);
idPledge = toPledge;
p = _findPledge(idPledge);
}
toPledge = _getOldestPledgeNotCanceled(idPledge);
if (toPledge != idPledge) {
_doTransfer(idPledge, toPledge, p.amount);
}
return toPledge;
}
////////////////////
// Internal methods
////////////////////
/// @notice A check to see if the msg.sender is the owner or the
/// plugin contract for a specific Admin
/// @param idAdmin The id of the admin being checked
function _checkAdminOwner(uint64 idAdmin) internal view {
PledgeAdmin storage a = _findAdmin(idAdmin);
require(msg.sender == address(a.plugin) || msg.sender == a.addr);
}
function _transfer(
uint64 idSender,
uint64 idPledge,
uint amount,
uint64 idReceiver
) internal
{
require(idReceiver > 0); // prevent burning value
idPledge = normalizePledge(idPledge);
Pledge storage p = _findPledge(idPledge);
PledgeAdmin storage receiver = _findAdmin(idReceiver);
require(p.pledgeState == PledgeState.Pledged);
// If the sender is the owner of the Pledge
if (p.owner == idSender) {
if (receiver.adminType == PledgeAdminType.Giver) {
_transferOwnershipToGiver(idPledge, amount, idReceiver);
return;
} else if (receiver.adminType == PledgeAdminType.Project) {
_transferOwnershipToProject(idPledge, amount, idReceiver);
return;
} else if (receiver.adminType == PledgeAdminType.Delegate) {
uint recieverDIdx = _getDelegateIdx(p, idReceiver);
if (p.intendedProject > 0 && recieverDIdx != NOTFOUND) {
// if there is an intendedProject and the receiver is in the delegationChain,
// then we want to preserve the delegationChain as this is a veto of the
// intendedProject by the owner
if (recieverDIdx == p.delegationChain.length - 1) {
uint64 toPledge = _findOrCreatePledge(
p.owner,
p.delegationChain,
0,
0,
p.oldPledge,
p.token,
PledgeState.Pledged);
_doTransfer(idPledge, toPledge, amount);
return;
}
_undelegate(idPledge, amount, p.delegationChain.length - receiverDIdx - 1);
return;
}
// owner is not vetoing an intendedProject and is transferring the pledge to a delegate,
// so we want to reset the delegationChain
idPledge = _undelegate(
idPledge,
amount,
p.delegationChain.length
);
_appendDelegate(idPledge, amount, idReceiver);
return;
}
// This should never be reached as the receiver.adminType
// should always be either a Giver, Project, or Delegate
assert(false);
}
// If the sender is a Delegate
uint senderDIdx = _getDelegateIdx(p, idSender);
if (senderDIdx != NOTFOUND) {
// And the receiver is another Giver
if (receiver.adminType == PledgeAdminType.Giver) {
// Only transfer to the Giver who owns the pledge
assert(p.owner == idReceiver);
_undelegate(idPledge, amount, p.delegationChain.length);
return;
}
// And the receiver is another Delegate
if (receiver.adminType == PledgeAdminType.Delegate) {
uint receiverDIdx = _getDelegateIdx(p, idReceiver);
// And not in the delegationChain or part of the delegationChain
// is after the sender, then all of the other delegates after
// the sender are removed and the receiver is appended at the
// end of the delegationChain
if (receiverDIdx == NOTFOUND || receiverDIdx > senderDIdx) {
idPledge = _undelegate(
idPledge,
amount,
p.delegationChain.length - senderDIdx - 1
);
_appendDelegate(idPledge, amount, idReceiver);
return;
}
// And is already part of the delegate chain but is before the
// sender, then the sender and all of the other delegates after
// the RECEIVER are removed from the delegationChain
_undelegate(
idPledge,
amount,
p.delegationChain.length - receiverDIdx - 1
);
return;
}
// And the receiver is a Project, all the delegates after the sender
// are removed and the amount is pre-committed to the project
if (receiver.adminType == PledgeAdminType.Project) {
idPledge = _undelegate(
idPledge,
amount,
p.delegationChain.length - senderDIdx - 1
);
_proposeAssignProject(idPledge, amount, idReceiver);
return;
}
}
assert(false); // When the sender is not an owner or a delegate
}
/// @notice `transferOwnershipToProject` allows for the transfer of
/// ownership to the project, but it can also be called by a project
/// to un-delegate everyone by setting one's own id for the idReceiver
/// @param idPledge the id of the pledge to be transfered.
/// @param amount Quantity of value that's being transfered
/// @param idReceiver The new owner of the project (or self to un-delegate)
function _transferOwnershipToProject(
uint64 idPledge,
uint amount,
uint64 idReceiver
) internal
{
Pledge storage p = _findPledge(idPledge);
// Ensure that the pledge is not already at max pledge depth
// and the project has not been canceled
require(_getPledgeLevel(p) < MAX_INTERPROJECT_LEVEL);
require(!isProjectCanceled(idReceiver));
uint64 oldPledge = _findOrCreatePledge(
p.owner,
p.delegationChain,
0,
0,
p.oldPledge,
p.token,
PledgeState.Pledged
);
uint64 toPledge = _findOrCreatePledge(
idReceiver, // Set the new owner
new uint64[](0), // clear the delegation chain
0,
0,
oldPledge,
p.token,
PledgeState.Pledged
);
_doTransfer(idPledge, toPledge, amount);
}
/// @notice `transferOwnershipToGiver` allows for the transfer of
/// value back to the Giver, value is placed in a pledged state
/// without being attached to a project, delegation chain, or time line.
/// @param idPledge the id of the pledge to be transferred.
/// @param amount Quantity of value that's being transferred
/// @param idReceiver The new owner of the pledge
function _transferOwnershipToGiver(
uint64 idPledge,
uint amount,
uint64 idReceiver
) internal
{
Pledge storage p = _findPledge(idPledge);
uint64 toPledge = _findOrCreatePledge(
idReceiver,
new uint64[](0),
0,
0,
0,
p.token,
PledgeState.Pledged
);
_doTransfer(idPledge, toPledge, amount);
}
/// @notice `appendDelegate` allows for a delegate to be added onto the
/// end of the delegate chain for a given Pledge.
/// @param idPledge the id of the pledge thats delegate chain will be modified.
/// @param amount Quantity of value that's being chained.
/// @param idReceiver The delegate to be added at the end of the chain
function _appendDelegate(
uint64 idPledge,
uint amount,
uint64 idReceiver
) internal
{
Pledge storage p = _findPledge(idPledge);
require(p.delegationChain.length < MAX_DELEGATES);
uint64[] memory newDelegationChain = new uint64[](
p.delegationChain.length + 1
);
for (uint i = 0; i < p.delegationChain.length; i++) {
newDelegationChain[i] = p.delegationChain[i];
}
// Make the last item in the array the idReceiver
newDelegationChain[p.delegationChain.length] = idReceiver;
uint64 toPledge = _findOrCreatePledge(
p.owner,
newDelegationChain,
0,
0,
p.oldPledge,
p.token,
PledgeState.Pledged
);
_doTransfer(idPledge, toPledge, amount);
}
/// @notice `appendDelegate` allows for a delegate to be added onto the
/// end of the delegate chain for a given Pledge.
/// @param idPledge the id of the pledge thats delegate chain will be modified.
/// @param amount Quantity of value that's shifted from delegates.
/// @param q Number (or depth) of delegates to remove
/// @return toPledge The id for the pledge being adjusted or created
function _undelegate(
uint64 idPledge,
uint amount,
uint q
) internal returns (uint64 toPledge)
{
Pledge storage p = _findPledge(idPledge);
uint64[] memory newDelegationChain = new uint64[](
p.delegationChain.length - q
);
for (uint i = 0; i < p.delegationChain.length - q; i++) {
newDelegationChain[i] = p.delegationChain[i];
}
toPledge = _findOrCreatePledge(
p.owner,
newDelegationChain,
0,
0,
p.oldPledge,
p.token,
PledgeState.Pledged
);
_doTransfer(idPledge, toPledge, amount);
}
/// @notice `proposeAssignProject` proposes the assignment of a pledge
/// to a specific project.
/// @dev This function should potentially be named more specifically.
/// @param idPledge the id of the pledge that will be assigned.
/// @param amount Quantity of value this pledge leader would be assigned.
/// @param idReceiver The project this pledge will potentially
/// be assigned to.
function _proposeAssignProject(
uint64 idPledge,
uint amount,
uint64 idReceiver
) internal
{
Pledge storage p = _findPledge(idPledge);
require(_getPledgeLevel(p) < MAX_INTERPROJECT_LEVEL);
require(!isProjectCanceled(idReceiver));
uint64 toPledge = _findOrCreatePledge(
p.owner,
p.delegationChain,
idReceiver,
uint64(_getTime() + _maxCommitTime(p)),
p.oldPledge,
p.token,
PledgeState.Pledged
);
_doTransfer(idPledge, toPledge, amount);
}
/// @notice `doTransfer` is designed to allow for pledge amounts to be
/// shifted around internally.
/// @param from This is the id of the pledge from which value will be transferred.
/// @param to This is the id of the pledge that value will be transferred to.
/// @param _amount The amount of value that will be transferred.
function _doTransfer(uint64 from, uint64 to, uint _amount) internal {
uint amount = _callPlugins(true, from, to, _amount);
if (from == to) {
return;
}
if (amount == 0) {
return;
}
Pledge storage pFrom = _findPledge(from);
Pledge storage pTo = _findPledge(to);
require(pFrom.amount >= amount);
pFrom.amount -= amount;
pTo.amount += amount;
require(pTo.amount >= amount);
Transfer(from, to, amount);
_callPlugins(false, from, to, amount);
}
/// @notice A getter to find the longest commitTime out of the owner and all
/// the delegates for a specified pledge
/// @param p The Pledge being queried
/// @return The maximum commitTime out of the owner and all the delegates
function _maxCommitTime(Pledge p) internal view returns(uint64 commitTime) {
PledgeAdmin storage a = _findAdmin(p.owner);
commitTime = a.commitTime; // start with the owner's commitTime
for (uint i = 0; i < p.delegationChain.length; i++) {
a = _findAdmin(p.delegationChain[i]);
// If a delegate's commitTime is longer, make it the new commitTime
if (a.commitTime > commitTime) {
commitTime = a.commitTime;
}
}
}
/// @notice A getter to find the oldest pledge that hasn't been canceled
/// @param idPledge The starting place to lookup the pledges
/// @return The oldest idPledge that hasn't been canceled (DUH!)
function _getOldestPledgeNotCanceled(
uint64 idPledge
) internal view returns(uint64)
{
if (idPledge == 0) {
return 0;
}
Pledge storage p = _findPledge(idPledge);
PledgeAdmin storage admin = _findAdmin(p.owner);
if (admin.adminType == PledgeAdminType.Giver) {
return idPledge;
}
assert(admin.adminType == PledgeAdminType.Project);
if (!isProjectCanceled(p.owner)) {
return idPledge;
}
return _getOldestPledgeNotCanceled(p.oldPledge);
}
/// @notice `callPlugin` is used to trigger the general functions in the
/// plugin for any actions needed before and after a transfer happens.
/// Specifically what this does in relation to the plugin is something
/// that largely depends on the functions of that plugin. This function
/// is generally called in pairs, once before, and once after a transfer.
/// @param before This toggle determines whether the plugin call is occurring
/// before or after a transfer.
/// @param adminId This should be the Id of the *trusted* individual
/// who has control over this plugin.
/// @param fromPledge This is the Id from which value is being transfered.
/// @param toPledge This is the Id that value is being transfered to.
/// @param context The situation that is triggering the plugin. See plugin
/// for a full description of contexts.
/// @param amount The amount of value that is being transfered.
function _callPlugin(
bool before,
uint64 adminId,
uint64 fromPledge,
uint64 toPledge,
uint64 context,
address token,
uint amount
) internal returns (uint allowedAmount)
{
uint newAmount;
allowedAmount = amount;
PledgeAdmin storage admin = _findAdmin(adminId);
// Checks admin has a plugin assigned and a non-zero amount is requested
if (address(admin.plugin) != 0 && allowedAmount > 0) {
// There are two separate functions called in the plugin.
// One is called before the transfer and one after
if (before) {
newAmount = admin.plugin.beforeTransfer(
adminId,
fromPledge,
toPledge,
context,
token,
amount
);
require(newAmount <= allowedAmount);
allowedAmount = newAmount;
} else {
admin.plugin.afterTransfer(
adminId,
fromPledge,
toPledge,
context,
token,
amount
);
}
}
}
/// @notice `callPluginsPledge` is used to apply plugin calls to
/// the delegate chain and the intended project if there is one.
/// It does so in either a transferring or receiving context based
/// on the `p` and `fromPledge` parameters.
/// @param before This toggle determines whether the plugin call is occuring
/// before or after a transfer.
/// @param idPledge This is the id of the pledge on which this plugin
/// is being called.
/// @param fromPledge This is the Id from which value is being transfered.
/// @param toPledge This is the Id that value is being transfered to.
/// @param amount The amount of value that is being transfered.
function _callPluginsPledge(
bool before,
uint64 idPledge,
uint64 fromPledge,
uint64 toPledge,
uint amount
) internal returns (uint allowedAmount)
{
// Determine if callPlugin is being applied in a receiving
// or transferring context
uint64 offset = idPledge == fromPledge ? 0 : 256;
allowedAmount = amount;
Pledge storage p = _findPledge(idPledge);
// Always call the plugin on the owner
allowedAmount = _callPlugin(
before,
p.owner,
fromPledge,
toPledge,
offset,
p.token,
allowedAmount
);
// Apply call plugin to all delegates
for (uint64 i = 0; i < p.delegationChain.length; i++) {
allowedAmount = _callPlugin(
before,
p.delegationChain[i],
fromPledge,
toPledge,
offset + i + 1,
p.token,
allowedAmount
);
}
// If there is an intended project also call the plugin in
// either a transferring or receiving context based on offset
// on the intended project
if (p.intendedProject > 0) {
allowedAmount = _callPlugin(
before,
p.intendedProject,
fromPledge,
toPledge,
offset + 255,
p.token,
allowedAmount
);
}
}
/// @notice `callPlugins` calls `callPluginsPledge` once for the transfer
/// context and once for the receiving context. The aggregated
/// allowed amount is then returned.
/// @param before This toggle determines whether the plugin call is occurring
/// before or after a transfer.
/// @param fromPledge This is the Id from which value is being transferred.
/// @param toPledge This is the Id that value is being transferred to.
/// @param amount The amount of value that is being transferred.
function _callPlugins(
bool before,
uint64 fromPledge,
uint64 toPledge,
uint amount
) internal returns (uint allowedAmount)
{
allowedAmount = amount;
// Call the plugins in the transfer context
allowedAmount = _callPluginsPledge(
before,
fromPledge,
fromPledge,
toPledge,
allowedAmount
);
// Call the plugins in the receive context
allowedAmount = _callPluginsPledge(
before,
toPledge,
fromPledge,
toPledge,
allowedAmount
);
}
/////////////
// Test functions
/////////////
/// @notice Basic helper function to return the current time
function _getTime() internal view returns (uint) {
return now;
}
}