Skip to content

Commit

Permalink
Merge pull request #24 from CirclesUBI/20231122-group-circles
Browse files Browse the repository at this point in the history
(circles): start implementing group circle node
  • Loading branch information
benjaminbollen authored Nov 27, 2023
2 parents 8139124 + 399f2af commit 5ed136c
Show file tree
Hide file tree
Showing 13 changed files with 462 additions and 152 deletions.
2 changes: 2 additions & 0 deletions specifications/TCIP005-group-circles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Native Group Circles

152 changes: 152 additions & 0 deletions src/circles/GroupCircle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.13;

import "./TemporalDiscount.sol";
import "./IGroup.sol";
import "../lib/Math64x64.sol";
import "../proxy/MasterCopyNonUpgradable.sol";
import "../graph/ICircleNode.sol";
import "../graph/IGraph.sol";

contract GroupCircle is MasterCopyNonUpgradable, TemporalDiscount, IGroupCircleNode {
// State variables

IGraph public graph;

// todo: we probably want group to have an interface so that we can call hooks on it
IGroup public group;

/**
* The exit fee (represented as 128bit 64.64 fixed point) is charged upon
* unwrapping the group circles back to the collateral personal circles.
* Note that one can only unwrap group circles for the amount of personal
* circles that are collateralized in the group, ie. the minimum of the amount
* of group circles you want to unwrap, and the amount of your personal
* circles that are collateralized in this group.
* If the the exit fee is set to the maximum of 100% (= 2**64 in 64.64),
* then upon minting the collateral is immediately burnt instead.
*/
int128 public exitFee_64x64;

/**
* Burn collateral upon minting is set to true,
* if the exit fee was set to 100%.
*/
bool public burnCollateralUponMinting;

// Modifiers

modifier onlyGraphOrGroup() {
require(
msg.sender == address(graph) || msg.sender == address(group), "Only graph or group can call this function."
);
_;
}

modifier onlyGraph() {
require(msg.sender == address(graph), "Only graph can call this function.");
_;
}

constructor() {
// block setup on the master copy deployment of the group circle
graph = IGraph(address(1));
}

// External functions

function setup(address _group, int128 _exitFee_64x64) external {
require(address(graph) == address(0), "Group circle contract has already been setup.");

require(address(_group) != address(0), "Group address must not be zero address");

require(_exitFee_64x64 <= ONE_64x64, "Exit fee can maximally be 100%.");
require(_exitFee_64x64 >= int128(0), "Exit fee can not be negative.");

if (_exitFee_64x64 == ONE_64x64) {
burnCollateralUponMinting = true;
} else {
burnCollateralUponMinting = false;
}

// graph contract must call setup after deploying proxy contract
graph = IGraph(msg.sender);
group = IGroup(_group);
creationTime = block.timestamp;
exitFee_64x64 = _exitFee_64x64;
}

function entity() external view returns (address entity_) {
return entity_ = address(group);
}

function pathTransfer(address _from, address _to, uint256 _amount) external onlyGraph {
// todo: should there be a hook here to call group?

_transfer(_from, _to, _amount);
}

// todo: does this mean something for group currencies?
function isActive() external pure returns (bool active_) {
return active_ = true;
}

function mint(ICircleNode[] calldata _collateral, uint256[] memory _amount) external {
require(_collateral.length == _amount.length, "Collateral and amount arrays must have equal length.");

require(_collateral.length > 0, "At least one collateral must be provided.");

// note: this is for code readability, this gets compiled out.
// To use group tokens as deposited collateral, they must be burnt.
// For example, if collateral is preserved one could redeposit
// (the same) group tokens, and the collateral would accumulate on
// what should be an idempotent function call.
// Burning the collateral prevents games to be played with inflated total collateral held by groups.
bool acceptGroupTokensAsCollateral = burnCollateralUponMinting;

require(
graph.checkAllAreTrustedCircleNodes(address(group), _collateral, acceptGroupTokensAsCollateral),
"All collateral must be valid circles on this graph."
);

// rely on group logic to evaluate whether minting should proceed:
// - the group can either revert to block the mint,
// - return `adjust = false` to proceed with the amounts as presented
// - or return `adjust = true` and provide an array of equal length
// which contains the factors (must be smaller or equal to one)
// by which each amount should be multiplied to proceed with the mint.
(bool adjust, int128[] memory adjustmentFactors) = group.beforeMintPolicy(msg.sender, _collateral, _amount);

if (adjust) {
require(adjustmentFactors.length == _amount.length, "Incorrect number of adjustment factors provided.");
for (uint256 i = 0; i < _amount.length; i++) {
// note: Math64x64.mulu will already require the factor is non-negative;
// but for clarity include the check here too (for now, optimise later).
require(
adjustmentFactors[i] <= ONE_64x64 && adjustmentFactors[i] >= int128(0),
"AdjustmentFactor must be between zero and one."
);
_amount[i] = Math64x64.mulu(adjustmentFactors[i], _amount[i]);
}
}

uint256 totalGroupCirclesToMint = uint256(0);

for (uint256 i = 0; i < _collateral.length; i++) {
_collateral[i].transferFrom(msg.sender, address(this), _amount[i]);
totalGroupCirclesToMint += _amount[i];
}

_mint(msg.sender, totalGroupCirclesToMint);

if (burnCollateralUponMinting) {
for (uint256 i = 0; i < _collateral.length; i++) {
_collateral[i].burn(_amount[i]);
}
}
}

function burn(uint256 _amount) external {
_burn(msg.sender, _amount);
}
}
12 changes: 12 additions & 0 deletions src/circles/IGroup.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.13;

import "../graph/ICircleNode.sol";

interface IGroup {
// todo: these are sketches of a simple interface
// should be considered again
function beforeMintPolicy(address minter, ICircleNode[] calldata collateral, uint256[] calldata amount)
external
returns (bool adjust, int128[] calldata adjustmentFactors);
}
50 changes: 32 additions & 18 deletions src/circles/TemporalDiscount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ contract TemporalDiscount is IERC20 {
/**
* Store the signed 128-bit 64.64 representation of 1 as a constant
*/
int128 private constant ONE_64x64 = int128(18446744073709551616);
int128 internal constant ONE_64x64 = int128(2**64);

/**
* Reduction factor gamma for temporally discounting balances
Expand Down Expand Up @@ -199,16 +199,20 @@ contract TemporalDiscount is IERC20 {
// but the pattern is off if we change the signature to pass currentSpan,
// todo: evaluate if it is worth splitting the signatures for this...
uint256 currentSpan = _currentTimeSpan();
// note: we don't discount the total supply before adding an amount
// because we account for discounts in the individual balances
// and already subtract those, so we should not double count.
temporalTotalSupply += _amount;
totalSupplyTime = currentSpan;
_discountTotalSupplyThenMint(_amount, currentSpan);
_discountBalanceThenAdd(_owner, _amount, currentSpan);

emit Transfer(address(0), _owner, _amount);
}

function _burn(address _owner, uint256 _amount) internal {
uint256 currentSpan = _currentTimeSpan();
_discountBalanceThenSubtract(_owner, _amount, currentSpan);
_discountTotalSupplyThenBurn(_amount, currentSpan);

emit Transfer(_owner, address(0), _amount);
}

/**
* @notice current time span returns the count of time spans (counted in weeks)
* that have passed since ZERO_TIME.
Expand Down Expand Up @@ -245,13 +249,10 @@ contract TemporalDiscount is IERC20 {
// and update the timespan in which we updated the balance.
balanceTimeSpans[_owner] = _currentSpan;

// adjust total supply to reflect discount cost
_subtractTotalSupply(discountCost_, _currentSpan);

// emit DiscountCost only when effectively discounted.
// if the original balance was zero before adding,
// discount cost can still be zero, even when discounted
// todo: possibly optimise? at cost of more if clauses?
if (discountCost_ != uint256(0)) {
emit DiscountCost(_owner, discountCost_);
}
Expand Down Expand Up @@ -283,21 +284,34 @@ contract TemporalDiscount is IERC20 {
// and update the timespan in which we updated the balance.
balanceTimeSpans[_owner] = _currentSpan;

// adjust total supply to reflect discount cost
_subtractTotalSupply(discountCost_, _currentSpan);

// emit DiscountCost only when effectively discounted.
// note: there must have been some discount cost, because we subtracted
// an amount from the balance successfully.
emit DiscountCost(_owner, discountCost_);
return discountCost_;
}
}

function _subtractTotalSupply(uint256 _discountCost, uint256 _currentSpan) private {
// note: we don't discount the total supply in write operations,
// because we already have accounted for the discounts
// in the costs that get subtracted.
temporalTotalSupply = temporalTotalSupply - _discountCost;
totalSupplyTime = _currentSpan;
function _discountTotalSupplyThenMint(uint256 _amount, uint256 _currentSpan) private {
if (totalSupplyTime == _currentSpan) {
temporalTotalSupply += _amount;
} else {
uint256 discountedTotalSupply =
_calculateDiscountedBalance(temporalTotalSupply, _currentSpan - totalSupplyTime);
temporalTotalSupply = discountedTotalSupply + _amount;
totalSupplyTime = _currentSpan;
}
}

function _discountTotalSupplyThenBurn(uint256 _amount, uint256 _currentSpan) private {
if (totalSupplyTime == _currentSpan) {
temporalTotalSupply -= _amount;
} else {
uint256 discountedTotalSupply =
_calculateDiscountedBalance(temporalTotalSupply, _currentSpan - totalSupplyTime);
temporalTotalSupply = discountedTotalSupply - _amount;
totalSupplyTime = _currentSpan;
}
}

function _calculateDiscountedBalance(uint256 _balance, uint256 _numberOfTimeSpans)
Expand Down
23 changes: 18 additions & 5 deletions src/circles/TimeCircle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "../proxy/MasterCopyNonUpgradable.sol";
import "../graph/ICircleNode.sol";
import "../graph/IGraph.sol";

contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, ICircleNode {
contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, IAvatarCircleNode {
// Constants

address public constant SENTINEL_MIGRATION = address(0x1);
Expand Down Expand Up @@ -86,10 +86,15 @@ contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, ICircleNode {
}

modifier notStopped() {
require(!stopped, "Node can not have been stopped.");
require(!stopped, "Node must not have been stopped.");
_;
}

constructor() {
// block the mastercopy from getting called setup on
graph = IGraph(address(1));
}

// External functions

function setup(address _avatar, bool _active, address[] calldata _migrations) external {
Expand All @@ -107,8 +112,7 @@ contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, ICircleNode {
lastIssuanceTimeSpan = _currentTimeSpan();

// instantiate the linked list
// todo: this is not necessary with a prepend-linked list?
// migrations[SENTINEL_MIGRATION] = SENTINEL_MIGRATION;
migrations[SENTINEL_MIGRATION] = SENTINEL_MIGRATION;

// loop over memory array to insert migration history into linked list
for (uint256 i = 0; i < _migrations.length; i++) {
Expand All @@ -126,6 +130,10 @@ contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, ICircleNode {
}
}

function entity() external view returns (address entity_) {
return entity_ = avatar;
}

function claimIssuance() external onlyActive {
uint256 currentSpan = _currentTimeSpan();
uint256 outstandingBalance = _calculateIssuance(currentSpan);
Expand Down Expand Up @@ -166,6 +174,10 @@ contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, ICircleNode {
return _calculateIssuance(_currentTimeSpan());
}

function burn(uint256 _amount) external {
_burn(msg.sender, _amount);
}

// Public functions

function isActive() public view returns (bool active_) {
Expand Down Expand Up @@ -226,13 +238,14 @@ contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, ICircleNode {
// Private function

function _insertMigration(address _migration) private {
assert(_migration != SENTINEL_MIGRATION);
require(_migration != address(0), "Migration address cannot be zero address.");
// idempotent under repeated insertion
if (migrations[_migration] != address(0)) {
return;
}
// prepend new migration address at beginning of linked list
migrations[_migration] = SENTINEL_MIGRATION;
migrations[_migration] = migrations[SENTINEL_MIGRATION];
migrations[SENTINEL_MIGRATION] = _migration;
}

Expand Down
Loading

0 comments on commit 5ed136c

Please sign in to comment.