Skip to content

Commit

Permalink
Merge pull request #48 from CirclesUBI/20231130-token-migration
Browse files Browse the repository at this point in the history
20231130 token migration
  • Loading branch information
benjaminbollen authored Dec 13, 2023
2 parents 797b2fd + 9c47eb3 commit e5693ab
Show file tree
Hide file tree
Showing 15 changed files with 364 additions and 57 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ This Solidity project uses Foundry as a toolkit. If you don't have Foundry insta
### Using Foundry to build the contracts
1. First, you'll need to clone the repository to your local machine:
```bash
git clone https://github.com/CirclesUBI/time-circles-contracts
cd time-circles-contracts
git clone https://github.com/CirclesUBI/circles-contracts-v2
cd circles-contracts-v2
```

### Compiling the contracts
Expand Down
2 changes: 1 addition & 1 deletion src/circles/TemporalDiscount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ contract TemporalDiscount is IERC20 {
/**
* EXA factor as 10^18
*/
uint256 internal constant EXA = uint256(1000000000000000000);
uint256 internal constant EXA = uint256(10 ** 18);

/**
* Store the signed 128-bit 64.64 representation of 1 as a constant
Expand Down
8 changes: 7 additions & 1 deletion src/circles/TimeCircle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, IAvatarCircleN
}

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

Expand Down Expand Up @@ -132,6 +132,12 @@ contract TimeCircle is MasterCopyNonUpgradable, TemporalDiscount, IAvatarCircleN
return _calculateIssuance(_currentTimeSpan());
}

function migrate(address _owner, uint256 _amount) external onlyGraph notStopped returns (uint256 migratedAmount_) {
// simply mint the migration amount if the Circle is not stopped
_mint(_owner, _amount);
return migratedAmount_ = _amount;
}

function burn(uint256 _amount) external {
_burn(msg.sender, _amount);
}
Expand Down
26 changes: 24 additions & 2 deletions src/graph/Graph.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ contract Graph is ProxyFactory, IGraph {
*/
IMintSplitter public immutable mintSplitter;

/**
* @notice Ancestor Circle Migrator contract can call on this graph to migrate
* Circles balance from an account on a Circle contract in Hub v1
* into the Circle contract of the same associated avatar.
*/
address public immutable ancestorCircleMigrator;

/**
* Master copy of the avatar circle node contract to deploy proxy's for
* when an avatar signs up.
Expand Down Expand Up @@ -137,10 +144,13 @@ contract Graph is ProxyFactory, IGraph {

event Trust(address indexed truster, address indexed trustee, uint256 expiryTime);

event PauseClaim(address indexed claimer, address indexed node);

// Modifiers

modifier onlyAncestorMigrator() {
require(msg.sender == ancestorCircleMigrator, "Only ancestor circle migrator contract can call this function.");
_;
}

modifier notOnTrustGraph(address _entity) {
require(
address(avatarToCircle[_entity]) == address(0) && address(organizations[_entity]) == address(0)
Expand Down Expand Up @@ -171,16 +181,19 @@ contract Graph is ProxyFactory, IGraph {

constructor(
IMintSplitter _mintSplitter,
address _ancestorCircleMigrator,
IAvatarCircleNode _masterCopyAvatarCircleNode,
IGroupCircleNode _masterCopyGroupCircleNode
) {
require(address(_mintSplitter) != address(0), "Mint Splitter contract must be provided.");
// ancestorCircleMigrator can be zero and left unspecified. It simply disables migration.
require(
address(_masterCopyAvatarCircleNode) != address(0), "Mastercopy for Avatar Circle Node must not be zero."
);
require(address(_masterCopyGroupCircleNode) != address(0), "Mastercopy for Group Circle Node must not be zero.");

mintSplitter = _mintSplitter;
ancestorCircleMigrator = _ancestorCircleMigrator;
masterCopyAvatarCircleNode = _masterCopyAvatarCircleNode;
masterCopyGroupCircleNode = _masterCopyGroupCircleNode;

Expand Down Expand Up @@ -255,6 +268,15 @@ contract Graph is ProxyFactory, IGraph {
emit Trust(msg.sender, _entity, earliestExpiry);
}

function migrateCircles(address _owner, uint256 _amount, IAvatarCircleNode _circle)
external
onlyAncestorMigrator
returns (uint256 migratedAmount_)
{
require(address(avatarCircleNodesIterable[_circle]) != address(0), "Circle is not registered in this graph.");
return migratedAmount_ = _circle.migrate(_owner, _amount);
}

function fetchAllocation(address _avatar) external returns (int128 allocation_, uint256 earliestTimestamp_) {
require(
address(avatarCircleNodesIterable[ICircleNode(msg.sender)]) != address(0),
Expand Down
3 changes: 3 additions & 0 deletions src/graph/ICircleNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ interface IAvatarCircleNode is ICircleNode {
function setup(address avatar) external;

function stopped() external view returns (bool stopped);

// only personal Circles from v1 can be migrated, as group circles were not native in v1
function migrate(address owner, uint256 amount) external returns (uint256 migratedAmount);
}

interface IGroupCircleNode is ICircleNode {
Expand Down
13 changes: 6 additions & 7 deletions src/graph/IGraph.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ pragma solidity >=0.8.13;
import "./ICircleNode.sol";

interface IGraph {
// function trust(address _avatar) external;
// function untrust(address _avatar) external;
function avatarToCircle(address avatar) external view returns (IAvatarCircleNode);

function checkAllAreTrustedCircleNodes(address group, ICircleNode[] calldata circles, bool includeGroups)
external
view
returns (bool allTrusted_);
returns (bool allTrusted);

function fetchAllocation(address _avatar) external returns (int128 allocation_, uint256 earliestTimestamp_);
function migrateCircles(address owner, uint256 amount, IAvatarCircleNode circle)
external
returns (uint256 migratedAmount);

// function checkAncestorMigrations(address _avatar)
// external
// returns (bool objectToStartMint_, address[] memory migrationTokens_);
function fetchAllocation(address avatar) external returns (int128 allocation, uint256 earliestTimestamp);
}
10 changes: 10 additions & 0 deletions src/migration/IHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ interface IHubV1 {
function limits(address truster, address trustee) external returns (uint256);

function trust(address trustee, uint256 limit) external;

function deployedAt() external view returns (uint256);
function initialIssuance() external view returns (uint256);
// function issuance() external view returns (uint256);
// function issuanceByStep(uint256 periods) external view returns (uint256);
function inflate(uint256 initial, uint256 periods) external view returns (uint256);
function inflation() external view returns (uint256);
function divisor() external view returns (uint256);
function period() external view returns (uint256);
function periods() external view returns (uint256);
}
4 changes: 3 additions & 1 deletion src/migration/IToken.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.13;

import "../circles/IERC20.sol";

/**
* @title IToken v1
* @author Circles UBI
* @notice legacy interface of Hub contract in Circles v1
*/
interface ITokenV1 {
interface ITokenV1 is IERC20 {
function owner() external view returns (address);

function stopped() external view returns (bool);
Expand Down
145 changes: 145 additions & 0 deletions src/migration/Migration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.13;

import "./IHub.sol";
import "./IToken.sol";
import "../graph/IGraph.sol";

contract CirclesMigration {
// Constant

uint256 private constant ACCURACY = uint256(10 ** 8);

// State variables

IHubV1 public immutable hubV1;

uint256 public immutable inflation;
uint256 public immutable divisor;
uint256 public immutable deployedAt;
uint256 public immutable initialIssuance;
uint256 public immutable period;

// Constructor

// see for context prior discussions on the conversion of CRC to TC,
// and some reference to the 8 CRC per day to 24 CRC per day gauge-reset
// https://aboutcircles.com/t/conversion-from-crc-to-time-circles-and-back/463
// the UI conversion used is found here:
// https://github.com/circlesland/timecircle/blob/master/src/index.ts
constructor(IHubV1 _hubV1) {
require(address(_hubV1) != address(0), "Hub v1 address can not be zero.");

hubV1 = _hubV1;

// from deployed v1 contract SHOULD return deployedAt = 1602786330
// (for reference 6:25:30 pm UTC | Thursday, October 15, 2020)
deployedAt = hubV1.deployedAt();
// from deployed v1 contract SHOULD return period = 31556952
// (equivalent to 365 days 5 hours 49 minutes 12 seconds)
// because the period is not a whole number of hours,
// the interval of hub v1 will not match the periodicity of any hour-based period in v2.
period = hubV1.period();

// note: currently these parameters are not used, remove them if they remain so

// from deployed v1 contract SHOULD return inflation = 107
inflation = hubV1.inflation();
// from deployed v1 contract SHOULD return divisor = 100
divisor = hubV1.divisor();
// from deployed v1 contract SHOULD return initialIssuance = 92592592592592
// (equivalent to 1/3 CRC per hour; original at launch 8 CRC per day)
// later it was decided that 24 CRC per day, or 1 CRC per hour should be the standard gauge
// and the correction was done at the interface level, so everyone sees their balance
// corrected for 24 CRC/day; we should hence adopt this correction in the token migration step.
initialIssuance = hubV1.initialIssuance();
}

// External functions

function convertAndMigrateFullBalanceOfCircles(ITokenV1 _originCircle, IGraph _destinationGraph)
external
returns (uint256 mintedAmount_)
{
uint256 balance = _originCircle.balanceOf(msg.sender);
return mintedAmount_ = convertAndMigrateCircles(_originCircle, balance, _destinationGraph);
}

// Public functions

/**
* @param _depositAmount Deposit amount specifies the amount of inflationary
* hub v1 circles the caller wants to convert and migrate to demurraged Circles.
* One can only convert personal v1 Circles, if that person has stopped their v1
* circles contract, and has created a v2 demurraged Circles contract by registering in v2.
*/
function convertAndMigrateCircles(ITokenV1 _originCircle, uint256 _depositAmount, IGraph _destinationGraph)
public
returns (uint256 mintedAmount_)
{
// First check the existance of the origin Circle, and associated avatar
address avatar = hubV1.tokenToUser(address(_originCircle));
require(avatar != address(0), "Origin Circle is unknown to hub v1.");

// and whether the origin Circle has been stopped.
require(_originCircle.stopped(), "Origin Circle must have been stopped before conversion.");

// Retrieve the destination Circle where to migrate the tokens to
IAvatarCircleNode destinationCircle = _destinationGraph.avatarToCircle(avatar);
// and check it in fact exists.
require(
address(destinationCircle) != address(0),
"Associated avatar has not been registered in the destination graph."
);

// Calculate inflationary correction towards time circles.
uint256 convertedAmount = convertFromV1ToTimeCircles(_depositAmount);

// transfer the tokens into a permanent lock in this contract
// v1 Circle does not have a burn function exposed, so we can only lock them here
_originCircle.transferFrom(msg.sender, address(this), _depositAmount);

require(
convertedAmount == _destinationGraph.migrateCircles(msg.sender, convertedAmount, destinationCircle),
"Destination graph must succeed at migrating the tokens."
);

return mintedAmount_ = convertedAmount;
}

function convertFromV1ToTimeCircles(uint256 _amount) public view returns (uint256 timeCircleAmount_) {
uint256 currentPeriod = hubV1.periods();
uint256 nextPeriod = currentPeriod + 1;

uint256 startOfPeriod = deployedAt + currentPeriod * period;

// number of seconds into the new period
uint256 secondsIntoCurrentPeriod = block.timestamp - startOfPeriod;

// rather than using initial issuance; use a clean order of magnitude
// to calculate the conversion factor.
// This is because initial issuance (originally ~ 8 CRC / day;
// then corrected to 24 CRC / day) is ever so slightly less than 1 CRC / hour
// ( 0.9999999999999936 CRC / hour to be precise )
// but if we later divide by this, then the error is ever so slightly
// in favor of converting - note there are many more errors,
// but we try to have each error always be in disadvantage of v1 so that
// there is no adverse incentive to mint and convert from v1
uint256 factorCurrentPeriod = hubV1.inflate(ACCURACY, currentPeriod);
uint256 factorNextPeriod = hubV1.inflate(ACCURACY, nextPeriod);

// linear interpolation of inflation rate
// r = x * (1 - a) + y * a
// if a = secondsIntoCurrentPeriod / Period = s / P
// => P * r = x * (P - s) + y * s
uint256 rP =
factorCurrentPeriod * (period - secondsIntoCurrentPeriod) + factorNextPeriod * secondsIntoCurrentPeriod;

// account for the adjustment of the accepted gauge of 24 CRC / day,
// rather than 8 CRC / day, so multiply by 3
// and divide by the inflation rate to convert to temporally discounted units
// (as if inflation would have been continuously adjusted. This is not the case,
// it is only annually compounded, but the disadvantage is for v1 vs v2).
return timeCircleAmount_ = (_amount * 3 * ACCURACY * period) / rP;
}
}
5 changes: 3 additions & 2 deletions test/graph/Graph.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import "../../src/graph/ICircleNode.sol";
import "../../src/circles/TimeCircle.sol";
import "../../src/circles/GroupCircle.sol";
import "../../src/mint/MintSplitter.sol";
import "./MockHub.sol";
import "../migration/MockHub.sol";
import "./MockInternalGraph.sol";

contract GraphTest is Test {
Expand Down Expand Up @@ -39,7 +39,8 @@ contract GraphTest is Test {

mintSplitter = new MintSplitter(mockHubV1);

graph = new Graph(mintSplitter, masterCopyTimeCircle, masterCopyGroupCircle);
// create a new graph without ancestor circle migration
graph = new Graph(mintSplitter, address(0), masterCopyTimeCircle, masterCopyGroupCircle);

mockInternalGraph = new MockInternalGraph(mintSplitter, masterCopyTimeCircle, masterCopyGroupCircle);
}
Expand Down
5 changes: 3 additions & 2 deletions test/graph/GraphPathTransfer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import "../../src/circles/TimeCircle.sol";
import "../../src/circles/GroupCircle.sol";
import "../../src/mint/MintSplitter.sol";
import "../setup/TimeSetup.sol";
import "./MockHub.sol";
import "../migration/MockHub.sol";

contract GraphPathTransferTest is Test, TimeSetup {
// Constant
Expand Down Expand Up @@ -47,7 +47,8 @@ contract GraphPathTransferTest is Test, TimeSetup {

mintSplitter = new MintSplitter(mockHubV1);

graph = new Graph(mintSplitter, masterCopyTimeCircle, masterCopyGroupCircle);
// create a new graph without ancestor circle migration
graph = new Graph(mintSplitter, address(0), masterCopyTimeCircle, masterCopyGroupCircle);

startTime();

Expand Down
38 changes: 0 additions & 38 deletions test/graph/MockHub.sol

This file was deleted.

Loading

0 comments on commit e5693ab

Please sign in to comment.