moving things around / cleanup
This commit is contained in:
parent
ece3355c67
commit
4cfebbc5fb
|
@ -1,96 +0,0 @@
|
|||
pragma solidity ^0.4.0;
|
||||
|
||||
import "node_modules/giveth-common-contracts/contracts/Escapable.sol";
|
||||
|
||||
contract EternalStorage is Escapable {
|
||||
|
||||
mapping(bytes32 => uint) UIntStorage;
|
||||
mapping(bytes32 => int) IntStorage;
|
||||
mapping(bytes32 => bool) BooleanStorage;
|
||||
mapping(bytes32 => address) AddressStorage;
|
||||
mapping(bytes32 => string) StringStorage;
|
||||
mapping(bytes32 => bytes) BytesStorage;
|
||||
mapping(bytes32 => bytes32) Bytes32Storage;
|
||||
|
||||
function EternalStorage(address _escapeHatchCaller, address _escapeHatchDestination)
|
||||
Escapable(_escapeHatchCaller, _escapeHatchDestination) public {
|
||||
}
|
||||
|
||||
/// UInt Storage
|
||||
|
||||
function getUIntValue(bytes32 record) public view returns (uint) {
|
||||
return UIntStorage[record];
|
||||
}
|
||||
|
||||
function setUIntValue(bytes32 record, uint value) public onlyOwner {
|
||||
UIntStorage[record] = value;
|
||||
}
|
||||
|
||||
function incrementUIntValue(bytes32 record) public returns (uint) {
|
||||
return UIntStorage[record] += 1;
|
||||
}
|
||||
|
||||
/// Int Storage
|
||||
|
||||
function getIntValue(bytes32 record) public view returns (int) {
|
||||
return IntStorage[record];
|
||||
}
|
||||
|
||||
function setIntValue(bytes32 record, int value) public onlyOwner {
|
||||
IntStorage[record] = value;
|
||||
}
|
||||
|
||||
function incrementIntValue(bytes32 record) public returns (int) {
|
||||
return IntStorage[record] += int(1);
|
||||
}
|
||||
|
||||
/// Address Storage
|
||||
|
||||
function getAddressValue(bytes32 record) public view returns (address) {
|
||||
return AddressStorage[record];
|
||||
}
|
||||
|
||||
function setAddressValue(bytes32 record, address value) public onlyOwner {
|
||||
AddressStorage[record] = value;
|
||||
}
|
||||
|
||||
/// String Storage
|
||||
|
||||
function getStringValue(bytes32 record) public view returns (string) {
|
||||
return StringStorage[record];
|
||||
}
|
||||
|
||||
function setStringValue(bytes32 record, string value) public onlyOwner {
|
||||
StringStorage[record] = value;
|
||||
}
|
||||
|
||||
/// Bytes Storage
|
||||
|
||||
function getBytesValue(bytes32 record) public view returns (bytes) {
|
||||
return BytesStorage[record];
|
||||
}
|
||||
|
||||
function setBytesValue(bytes32 record, bytes value) public onlyOwner {
|
||||
BytesStorage[record] = value;
|
||||
}
|
||||
|
||||
/// Bytes Storage
|
||||
|
||||
function getBytes32Value(bytes32 record) public view returns (bytes32) {
|
||||
return Bytes32Storage[record];
|
||||
}
|
||||
|
||||
function setBytes32Value(bytes32 record, bytes32 value) public onlyOwner {
|
||||
Bytes32Storage[record] = value;
|
||||
}
|
||||
|
||||
/// Boolean Storage
|
||||
|
||||
function getBooleanValue(bytes32 record) public view returns (bool) {
|
||||
return BooleanStorage[record];
|
||||
}
|
||||
|
||||
function setBooleanValue(bytes32 record, bool value) public onlyOwner {
|
||||
BooleanStorage[record] = value;
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
pragma solidity ^0.4.0;
|
||||
|
||||
import "./EternalStorage.sol";
|
||||
|
||||
library EternallyPersistentLib {
|
||||
|
||||
// UInt
|
||||
//TODO if we use assembly here, we can save ~ 600 gas / call, due to skipping the extcodesize check that solidity adds.
|
||||
|
||||
function stgObjectGetUInt(EternalStorage _storage, string class, uint id, string fieldName) internal view returns (uint) {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.getUIntValue(record);
|
||||
}
|
||||
|
||||
function stgObjectSetUInt(EternalStorage _storage, string class, uint id, string fieldName, uint value) internal {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.setUIntValue(record, value);
|
||||
}
|
||||
|
||||
// Boolean
|
||||
|
||||
function stgObjectGetBoolean(EternalStorage _storage, string class, uint id, string fieldName) internal view returns (bool) {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.getBooleanValue(record);
|
||||
}
|
||||
|
||||
function stgObjectSetBoolean(EternalStorage _storage, string class, uint id, string fieldName, bool value) internal {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.setBooleanValue(record, value);
|
||||
}
|
||||
|
||||
// string
|
||||
|
||||
function stgObjectGetString(EternalStorage _storage, string class, uint id, string fieldName) internal view returns (string) {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
bytes4 sig = 0xa209a29c; // bytes4(keccak256("getStringValue(bytes32)"));
|
||||
string memory s;
|
||||
|
||||
assembly {
|
||||
let ptr := mload(0x40) //Find empty storage location using "free memory pointer"
|
||||
mstore(ptr, sig) //Place signature at beginning of empty storage
|
||||
mstore(add(ptr, 0x04), record) //Place first argument directly next to signature
|
||||
|
||||
let result := staticcall(sub(gas, 10000), _storage, ptr, 0x24, 0, 0)
|
||||
|
||||
let size := returndatasize
|
||||
returndatacopy(ptr, 0, size) // overwrite ptr to save a bit of gas
|
||||
|
||||
// revert instead of invalid() bc if the underlying call failed with invalid() it already wasted gas.
|
||||
// if the call returned error data, forward it
|
||||
switch result case 0 { revert(ptr, size) }
|
||||
default {
|
||||
mstore(0x40, add(ptr, size)) // update free mem location
|
||||
// We need to skip the first 32 bytes which contains the start offset of the returned byte array
|
||||
s := add(ptr, 0x20) // set the string
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function stgObjectSetString(EternalStorage _storage, string class, uint id, string fieldName, string value) internal {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.setStringValue(record, value);
|
||||
}
|
||||
|
||||
// address
|
||||
|
||||
function stgObjectGetAddress(EternalStorage _storage, string class, uint id, string fieldName) internal view returns (address) {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.getAddressValue(record);
|
||||
}
|
||||
|
||||
function stgObjectSetAddress(EternalStorage _storage, string class, uint id, string fieldName, address value) internal {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.setAddressValue(record, value);
|
||||
}
|
||||
|
||||
// bytes32
|
||||
|
||||
function stgObjectGetBytes32(EternalStorage _storage, string class, uint id, string fieldName) internal view returns (bytes32) {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.getBytes32Value(record);
|
||||
}
|
||||
|
||||
function stgObjectSetBytes32(EternalStorage _storage, string class, uint id, string fieldName, bytes32 value) internal {
|
||||
bytes32 record = keccak256(class, id, fieldName);
|
||||
return _storage.setBytes32Value(record, value);
|
||||
}
|
||||
|
||||
// Array
|
||||
|
||||
function stgCollectionAddItem(EternalStorage _storage, bytes32 idArray) internal returns (uint) {
|
||||
return _storage.incrementUIntValue(keccak256(idArray, "length"));
|
||||
}
|
||||
|
||||
function stgCollectionLength(EternalStorage _storage, bytes32 idArray) internal view returns (uint) {
|
||||
return _storage.getUIntValue(keccak256(idArray, "length"));
|
||||
}
|
||||
|
||||
function stgCollectionIdFromIdx(EternalStorage _storage, bytes32 idArray, uint idx) internal view returns (bytes32) {
|
||||
return _storage.getBytes32Value(keccak256(idArray, idx));
|
||||
}
|
||||
|
||||
function stgUpgrade(EternalStorage _storage, address newContract) internal {
|
||||
_storage.changeOwnership(newContract);
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@ import "./LiquidPledgingBase.sol";
|
|||
/// to allow for expanded functionality.
|
||||
contract LiquidPledging is LiquidPledgingBase {
|
||||
|
||||
|
||||
/// @notice This is how value enters the system and how pledges are created;
|
||||
/// the ether is sent to the vault, an pledge for the Giver is created (or
|
||||
/// found), the amount of ETH donated in wei is added to the `amount` in
|
||||
|
@ -39,7 +38,6 @@ contract LiquidPledging is LiquidPledgingBase {
|
|||
function donate(uint64 idGiver, uint64 idReceiver)
|
||||
public payable
|
||||
{
|
||||
// TODO: maybe need a separate role here to allow giver creation
|
||||
if (idGiver == 0) {
|
||||
// default to a 3 day (259200 seconds) commitTime
|
||||
idGiver = uint64(addGiver("", "", 259200, ILiquidPledgingPlugin(0x0)));
|
||||
|
@ -93,9 +91,7 @@ contract LiquidPledging is LiquidPledgingBase {
|
|||
|
||||
Pledges.Pledge storage p = _findPledge(idPledge);
|
||||
PledgeAdmins.PledgeAdmin storage receiver = _findAdmin(idReceiver);
|
||||
// PledgeAdmins.PledgeAdmin storage sender = _findAdmin(idSender);
|
||||
|
||||
// _checkAdminOwner(sender);
|
||||
require(p.pledgeState == PledgeState.Pledged);
|
||||
|
||||
// If the sender is the owner of the Pledge
|
||||
|
@ -128,7 +124,7 @@ contract LiquidPledging is LiquidPledgingBase {
|
|||
} else {
|
||||
// owner is not vetoing an intendedProject and is transferring the pledge to a delegate,
|
||||
// so we want to reset the delegationChain
|
||||
idPledge = _undelegate(
|
||||
idPledge = _undelegate(
|
||||
idPledge,
|
||||
amount,
|
||||
p.delegationChain.length
|
||||
|
@ -241,7 +237,6 @@ contract LiquidPledging is LiquidPledgingBase {
|
|||
/// @param idPledge Id of the pledge that is to be withdrawn
|
||||
/// @param amount Quantity of ether (in wei) to be withdrawn
|
||||
function confirmPayment(uint64 idPledge, uint amount) public onlyVault {
|
||||
//TODO don't fetch entire pledge
|
||||
Pledges.Pledge storage p = _findPledge(idPledge);
|
||||
|
||||
require(p.pledgeState == Pledges.PledgeState.Paying);
|
||||
|
@ -297,15 +292,13 @@ contract LiquidPledging is LiquidPledgingBase {
|
|||
/// @param idPledge Id of the pledge that is to be canceled
|
||||
/// @param amount Quantity of ether (in wei) to be transfered to the
|
||||
/// `oldPledge`
|
||||
function cancelPledge(uint64 idPledge, uint amount) public {//authP(PLEDGE_ADMIN_ROLE, arr(uint(idPledge))) public {
|
||||
function cancelPledge(uint64 idPledge, uint amount) public {
|
||||
idPledge = normalizePledge(idPledge);
|
||||
|
||||
Pledges.Pledge storage p = _findPledge(idPledge);
|
||||
require(p.oldPledge != 0);
|
||||
|
||||
require(canPerform(msg.sender, PLEDGE_ADMIN_ROLE, arr(uint(p.owner))));
|
||||
// PledgeAdmins.PledgeAdmin storage a = _findAdmin(p.owner);
|
||||
// _checkAdminOwner(a);
|
||||
|
||||
uint64 oldPledge = _getOldestPledgeNotCanceled(p.oldPledge);
|
||||
_doTransfer(idPledge, oldPledge, amount);
|
||||
|
@ -396,192 +389,6 @@ contract LiquidPledging is LiquidPledgingBase {
|
|||
}
|
||||
}
|
||||
|
||||
////////
|
||||
// Private methods
|
||||
///////
|
||||
|
||||
/// @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
|
||||
{
|
||||
Pledges.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,
|
||||
Pledges.PledgeState.Pledged
|
||||
);
|
||||
uint64 toPledge = _findOrCreatePledge(
|
||||
idReceiver, // Set the new owner
|
||||
new uint64[](0), // clear the delegation chain
|
||||
0,
|
||||
0,
|
||||
uint64(oldPledge),
|
||||
Pledges.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 transfered.
|
||||
/// @param amount Quantity of value that's being transfered
|
||||
/// @param idReceiver The new owner of the pledge
|
||||
function _transferOwnershipToGiver(
|
||||
uint64 idPledge,
|
||||
uint amount,
|
||||
uint64 idReceiver
|
||||
) internal
|
||||
{
|
||||
uint64 toPledge = _findOrCreatePledge(
|
||||
idReceiver,
|
||||
new uint64[](0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
Pledges.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
|
||||
{
|
||||
Pledges.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,
|
||||
Pledges.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)
|
||||
{
|
||||
Pledges.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,
|
||||
Pledges.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
|
||||
{
|
||||
Pledges.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,
|
||||
Pledges.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 transfered.
|
||||
/// @param to This is the id of the pledge that value will be transfered to.
|
||||
/// @param _amount The amount of value that will be transfered.
|
||||
function _doTransfer(uint64 from, uint64 to, uint _amount) internal {
|
||||
uint amount = _callPlugins(true, from, to, _amount);
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (amount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pledges.Pledge storage pFrom = _findPledge(from);
|
||||
Pledges.Pledge storage pTo = _findPledge(to);
|
||||
|
||||
require(pFrom.amount >= amount);
|
||||
pFrom.amount -= amount;
|
||||
pTo.amount += amount;
|
||||
|
||||
Transfer(from, to, amount);
|
||||
_callPlugins(false, from, to, amount);
|
||||
}
|
||||
|
||||
/// @notice Only affects pledges with the Pledged Pledges.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
|
||||
|
@ -638,176 +445,4 @@ contract LiquidPledging is LiquidPledgingBase {
|
|||
|
||||
return toPledge;
|
||||
}
|
||||
|
||||
/////////////
|
||||
// Plugins
|
||||
/////////////
|
||||
|
||||
/// @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,
|
||||
uint amount
|
||||
) internal returns (uint allowedAmount) {
|
||||
|
||||
uint newAmount;
|
||||
allowedAmount = amount;
|
||||
PledgeAdmins.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 seperate functions called in the plugin.
|
||||
// One is called before the transfer and one after
|
||||
if (before) {
|
||||
newAmount = admin.plugin.beforeTransfer(
|
||||
adminId,
|
||||
fromPledge,
|
||||
toPledge,
|
||||
context,
|
||||
amount
|
||||
);
|
||||
require(newAmount <= allowedAmount);
|
||||
allowedAmount = newAmount;
|
||||
} else {
|
||||
admin.plugin.afterTransfer(
|
||||
adminId,
|
||||
fromPledge,
|
||||
toPledge,
|
||||
context,
|
||||
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;
|
||||
Pledges.Pledge storage p = _findPledge(idPledge);
|
||||
|
||||
// Always call the plugin on the owner
|
||||
allowedAmount = _callPlugin(
|
||||
before,
|
||||
p.owner,
|
||||
fromPledge,
|
||||
toPledge,
|
||||
offset,
|
||||
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,
|
||||
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,
|
||||
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 pledges plugins in the transfer context
|
||||
allowedAmount = _callPluginsPledge(
|
||||
before,
|
||||
fromPledge,
|
||||
fromPledge,
|
||||
toPledge,
|
||||
allowedAmount
|
||||
);
|
||||
|
||||
// Call the pledges 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;
|
||||
}
|
||||
|
||||
function getTime() public view returns(uint) {
|
||||
return _getTime();
|
||||
}
|
||||
|
||||
// Event Declarations
|
||||
event Transfer(uint indexed from, uint indexed to, uint amount);
|
||||
event CancelProject(uint indexed idProject);
|
||||
|
||||
}
|
||||
|
|
|
@ -38,6 +38,10 @@ interface ILPVault {
|
|||
/// data structures
|
||||
contract LiquidPledgingBase is PledgeAdmins, Pledges, EscapableApp {
|
||||
|
||||
// Event Declarations
|
||||
event Transfer(uint indexed from, uint indexed to, uint amount);
|
||||
event CancelProject(uint indexed idProject);
|
||||
|
||||
ILPVault public vault;
|
||||
|
||||
/////////////
|
||||
|
@ -51,7 +55,6 @@ contract LiquidPledgingBase is PledgeAdmins, Pledges, EscapableApp {
|
|||
_;
|
||||
}
|
||||
|
||||
|
||||
///////////////
|
||||
// Constructor
|
||||
///////////////
|
||||
|
@ -99,12 +102,187 @@ contract LiquidPledgingBase is PledgeAdmins, Pledges, EscapableApp {
|
|||
// Internal methods
|
||||
////////////////////
|
||||
|
||||
/// @notice A check to see if the msg.sender is the owner or the
|
||||
/// plugin contract for a specific Admin
|
||||
/// @param a The admin being checked
|
||||
// function _checkAdminOwner(PledgeAdmin a) internal constant {
|
||||
// require(msg.sender == a.addr || msg.sender == address(a.plugin));
|
||||
// }
|
||||
/// @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
|
||||
{
|
||||
Pledges.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,
|
||||
Pledges.PledgeState.Pledged
|
||||
);
|
||||
uint64 toPledge = _findOrCreatePledge(
|
||||
idReceiver, // Set the new owner
|
||||
new uint64[](0), // clear the delegation chain
|
||||
0,
|
||||
0,
|
||||
uint64(oldPledge),
|
||||
Pledges.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 transfered.
|
||||
/// @param amount Quantity of value that's being transfered
|
||||
/// @param idReceiver The new owner of the pledge
|
||||
function _transferOwnershipToGiver(
|
||||
uint64 idPledge,
|
||||
uint amount,
|
||||
uint64 idReceiver
|
||||
) internal
|
||||
{
|
||||
uint64 toPledge = _findOrCreatePledge(
|
||||
idReceiver,
|
||||
new uint64[](0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
Pledges.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
|
||||
{
|
||||
Pledges.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,
|
||||
Pledges.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)
|
||||
{
|
||||
Pledges.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,
|
||||
Pledges.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
|
||||
{
|
||||
Pledges.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,
|
||||
Pledges.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 transfered.
|
||||
/// @param to This is the id of the pledge that value will be transfered to.
|
||||
/// @param _amount The amount of value that will be transfered.
|
||||
function _doTransfer(uint64 from, uint64 to, uint _amount) internal {
|
||||
uint amount = _callPlugins(true, from, to, _amount);
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (amount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pledges.Pledge storage pFrom = _findPledge(from);
|
||||
Pledges.Pledge storage pTo = _findPledge(to);
|
||||
|
||||
require(pFrom.amount >= amount);
|
||||
pFrom.amount -= amount;
|
||||
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
|
||||
|
@ -149,4 +327,165 @@ contract LiquidPledgingBase is PledgeAdmins, Pledges, EscapableApp {
|
|||
|
||||
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,
|
||||
uint amount
|
||||
) internal returns (uint allowedAmount)
|
||||
{
|
||||
|
||||
uint newAmount;
|
||||
allowedAmount = amount;
|
||||
PledgeAdmins.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 seperate functions called in the plugin.
|
||||
// One is called before the transfer and one after
|
||||
if (before) {
|
||||
newAmount = admin.plugin.beforeTransfer(
|
||||
adminId,
|
||||
fromPledge,
|
||||
toPledge,
|
||||
context,
|
||||
amount
|
||||
);
|
||||
require(newAmount <= allowedAmount);
|
||||
allowedAmount = newAmount;
|
||||
} else {
|
||||
admin.plugin.afterTransfer(
|
||||
adminId,
|
||||
fromPledge,
|
||||
toPledge,
|
||||
context,
|
||||
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;
|
||||
Pledges.Pledge storage p = _findPledge(idPledge);
|
||||
|
||||
// Always call the plugin on the owner
|
||||
allowedAmount = _callPlugin(
|
||||
before,
|
||||
p.owner,
|
||||
fromPledge,
|
||||
toPledge,
|
||||
offset,
|
||||
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,
|
||||
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,
|
||||
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 pledges plugins in the transfer context
|
||||
allowedAmount = _callPluginsPledge(
|
||||
before,
|
||||
fromPledge,
|
||||
fromPledge,
|
||||
toPledge,
|
||||
allowedAmount
|
||||
);
|
||||
|
||||
// Call the pledges 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
pragma solidity ^0.4.18;
|
||||
|
||||
import "./EternalStorage.sol";
|
||||
|
||||
contract LiquidPledgingStorage {
|
||||
|
||||
EternalStorage public _storage;
|
||||
|
||||
function LiquidPledgingStorage(address _s) public {
|
||||
_storage = EternalStorage(_s);
|
||||
}
|
||||
}
|
6
index.js
6
index.js
|
@ -1,4 +1,4 @@
|
|||
exports.LiquidPledging = require('./lib/liquidPledging.js');
|
||||
exports.LiquidPledgingMock = require('./lib/liquidPledgingMock.js');
|
||||
const contracts = require('./build/contracts');
|
||||
exports.LiquidPledging = contracts.LiquidPledging;
|
||||
exports.LiquidPledgingState = require('./lib/liquidPledgingState.js');
|
||||
exports.LPVault = require('./lib/vault.js');
|
||||
exports.LPVault = contracts.LPVault;
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
const EternalStorageAbi = require('../build/EternalStorage.sol').EternalStorageAbi;
|
||||
const EternalStorageCode = require('../build/EternalStorage.sol').EternalStorageByteCode;
|
||||
const generateClass = require('eth-contract-class').default;
|
||||
|
||||
module.exports = generateClass(EternalStorageAbi, EternalStorageCode);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
const LiquidPledgingAbi = require('../build/LiquidPledging.sol').LiquidPledgingAbi;
|
||||
const LiquidPledgingCode = require('../build/LiquidPledging.sol').LiquidPledgingByteCode;
|
||||
const generateClass = require('eth-contract-class').default;
|
||||
|
||||
module.exports = generateClass(LiquidPledgingAbi, LiquidPledgingCode);
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
const LiquidPledgingMockAbi = require('../build/LiquidPledgingMock.sol').LiquidPledgingMockAbi;
|
||||
const LiquidPledgingMockCode = require('../build/LiquidPledgingMock.sol').LiquidPledgingMockByteCode;
|
||||
const generateClass = require('eth-contract-class').default;
|
||||
|
||||
module.exports = generateClass(LiquidPledgingMockAbi, LiquidPledgingMockCode);
|
|
@ -1,5 +0,0 @@
|
|||
const LPVaultAbi = require('../build/LPVault.sol').LPVaultAbi;
|
||||
const LPVaultByteCode = require('../build/LPVault.sol').LPVaultByteCode;
|
||||
const generateClass = require('eth-contract-class').default;
|
||||
|
||||
module.exports = generateClass(LPVaultAbi, LPVaultByteCode);
|
|
@ -32,14 +32,13 @@ describe('LiquidPledging plugins test', function () {
|
|||
|
||||
before(async () => {
|
||||
testrpc = TestRPC.server({
|
||||
ws: true,
|
||||
gasLimit: 6700000,
|
||||
total_accounts: 10,
|
||||
});
|
||||
|
||||
testrpc.listen(8546, '127.0.0.1');
|
||||
testrpc.listen(8545, '127.0.0.1');
|
||||
|
||||
web3 = new Web3('ws://localhost:8546');
|
||||
web3 = new Web3('http://localhost:8545');
|
||||
accounts = await web3.eth.getAccounts();
|
||||
giver1 = accounts[1];
|
||||
adminProject1 = accounts[2];
|
||||
|
|
|
@ -29,14 +29,13 @@ describe('LiquidPledging cancelPledge normal scenario', function () {
|
|||
|
||||
before(async () => {
|
||||
testrpc = TestRPC.server({
|
||||
ws: true,
|
||||
gasLimit: 6700000,
|
||||
total_accounts: 10,
|
||||
});
|
||||
|
||||
testrpc.listen(8546, '127.0.0.1');
|
||||
testrpc.listen(8545, '127.0.0.1');
|
||||
|
||||
web3 = new Web3('ws://localhost:8546');
|
||||
web3 = new Web3('http://localhost:8545');
|
||||
accounts = await web3.eth.getAccounts();
|
||||
giver1 = accounts[ 1 ];
|
||||
adminProject1 = accounts[ 2 ];
|
||||
|
|
|
@ -33,14 +33,13 @@ describe('DelegationChain test', function () {
|
|||
const gasUsage = {};
|
||||
before(async () => {
|
||||
testrpc = TestRPC.server({
|
||||
ws: true,
|
||||
gasLimit: 6700000,
|
||||
total_accounts: 10,
|
||||
});
|
||||
|
||||
testrpc.listen(8545, '127.0.0.1');
|
||||
|
||||
web3 = new Web3('ws://localhost:8545');
|
||||
web3 = new Web3('http://localhost:8545');
|
||||
accounts = await web3.eth.getAccounts();
|
||||
giver1 = accounts[1];
|
||||
delegate1 = accounts[2];
|
||||
|
|
|
@ -39,14 +39,13 @@ describe('LiquidPledging test', function () {
|
|||
|
||||
before(async () => {
|
||||
testrpc = TestRPC.server({
|
||||
ws: true,
|
||||
gasLimit: 6700000,
|
||||
total_accounts: 11,
|
||||
});
|
||||
|
||||
testrpc.listen(8545, '127.0.0.1');
|
||||
|
||||
web3 = new Web3('ws://localhost:8545');
|
||||
web3 = new Web3('http://localhost:8545');
|
||||
accounts = await web3.eth.getAccounts();
|
||||
giver1 = accounts[1];
|
||||
delegate1 = accounts[2];
|
||||
|
|
|
@ -31,14 +31,13 @@ describe('NormalizePledge test', function () {
|
|||
|
||||
before(async () => {
|
||||
testrpc = TestRPC.server({
|
||||
ws: true,
|
||||
gasLimit: 6700000,
|
||||
total_accounts: 10,
|
||||
});
|
||||
|
||||
testrpc.listen(8546, '127.0.0.1');
|
||||
testrpc.listen(8545, '127.0.0.1');
|
||||
|
||||
web3 = new Web3('ws://localhost:8546');
|
||||
web3 = new Web3('http://localhost:8545');
|
||||
accounts = await web3.eth.getAccounts();
|
||||
giver1 = accounts[1];
|
||||
delegate1 = accounts[2];
|
||||
|
|
|
@ -27,14 +27,13 @@ describe('Vault test', function () {
|
|||
|
||||
before(async () => {
|
||||
testrpc = TestRPC.server({
|
||||
ws: true,
|
||||
gasLimit: 6700000,
|
||||
total_accounts: 10,
|
||||
});
|
||||
|
||||
testrpc.listen(8546, '127.0.0.1');
|
||||
testrpc.listen(8545, '127.0.0.1');
|
||||
|
||||
web3 = new Web3('ws://localhost:8546');
|
||||
web3 = new Web3('http://localhost:8545');
|
||||
accounts = await web3.eth.getAccounts();
|
||||
giver1 = accounts[ 1 ];
|
||||
adminProject1 = accounts[ 2 ];
|
||||
|
|
Loading…
Reference in New Issue