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

20231130 token migration #48

Merged
merged 11 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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