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

(migration): redo migration for hub v2 #114

Merged
merged 10 commits into from
Mar 7, 2024
40 changes: 1 addition & 39 deletions src/circles/Circles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,44 +114,6 @@ contract Circles is ERC1155 {

// Public functions

// todo: do personalBurn from hub only, don't burn group Circles; they must be redeemed first

// /**
// * @notice Burn Circles in demurrage units.
// * @param _id Circles identifier for which to burn the Circles.
// * @param _value Demurraged value of the Circles to burn.
// */
// function burn(uint256 _id, uint256 _value) public {
// assert(false);
// }

// /**
// * @notice Burn a batch of Circles in demurrage units.
// * @param _ids Batch of Circles identifiers for which to burn the Circles.
// * @param _values Batch of demurraged values of the Circles to burn.
// */
// function burnBatch(uint256[] memory _ids, uint256[] memory _values) public {
// assert(false);
// }

// /**
// * @notice Burn Circles in inflationary units.
// * @param _id Circles identifier for which to burn the Circles.
// * @param _value Value of the Circles to burn in inflationary units.
// */
// function inflationaryBurn(uint256 _id, uint256 _value) public {
// assert(false);
// }

// /**
// * @notice Burn a batch of Circles in inflationary units.
// * @param _ids Batch of Circles identifiers for which to burn the Circles.
// * @param _values Batch of values of the Circles to burn in inflationary units.
// */
// function inflationaryBurnBatch(uint256[] memory _ids, uint256[] memory _values) public {
// assert(false);
// }

/**
* @notice Calculate the demurraged issuance for a human's avatar.
* @param _human Address of the human's avatar to calculate the issuance for.
Expand Down Expand Up @@ -204,7 +166,7 @@ contract Circles is ERC1155 {
uint256 issuance = calculateIssuance(_human);
require(issuance > 0, "No issuance to claim.");
// mint personal Circles to the human
_mint(_human, super.toTokenId(_human), issuance, "");
_mint(_human, toTokenId(_human), issuance, "");
// update the last mint time
mintTimes[_human].lastMintTime = uint96(block.timestamp);
}
Expand Down
84 changes: 77 additions & 7 deletions src/hub/Hub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ contract Hub is Circles, IHubV2 {
// Constants

/**
* @dev Welcome bonus for new avatars invited to Circles. Set to three days of non-demurraged Circles.
* @dev Welcome bonus for new avatars invited to Circles. Set to 50 Circles.
*/
uint256 public constant WELCOME_BONUS = 3 * 24 * 10 ** 18;
uint256 public constant WELCOME_BONUS = 50 * EXA;

/**
* @dev The cost of an invitation for a new avatar, paid in personal Circles burnt, set to 100 Circles.
*/
uint256 public constant INVITATION_COST = 2 * WELCOME_BONUS;

/**
* @dev The minimum donation amount for registering a human as an organization,
Expand All @@ -62,6 +67,11 @@ contract Hub is Circles, IHubV2 {
*/
IHubV1 public immutable hubV1;

/**
* @notice The address of the migration contract for v1 Circles.
*/
address public immutable migration;

/**
* @notice The timestamp of the start of the invitation-only period.
* @dev This is used to determine the start of the invitation-only period.
Expand Down Expand Up @@ -125,7 +135,15 @@ contract Hub is Circles, IHubV2 {
* Modifier to check if the current time is during the bootstrap period.
*/
modifier duringBootstrap() {
require(block.timestamp < invitationOnlyTime, "Bootstrap period has ended.");
require(block.timestamp <= invitationOnlyTime, "Bootstrap period has ended.");
_;
}

/**
* Modifier to check if the caller is the migration contract.
*/
modifier onlyMigration() {
require(msg.sender == migration, "Only migration contract can call this function.");
_;
}

Expand All @@ -144,6 +162,7 @@ contract Hub is Circles, IHubV2 {
*/
constructor(
IHubV1 _hubV1,
address _migration,
uint256 _inflation_day_zero,
address _standardTreasury,
uint256 _bootstrapTime,
Expand All @@ -158,6 +177,9 @@ contract Hub is Circles, IHubV2 {
// store the Hub v1 contract address
hubV1 = _hubV1;

// store the migration contract address
migration = _migration;

// store the standard treasury contract address for registerGroup()
standardTreasury = _standardTreasury;

Expand Down Expand Up @@ -199,11 +221,13 @@ contract Hub is Circles, IHubV2 {
// register the invited human; reverts if they already exist
_registerHuman(_human);

// inviter must burn twice the welcome bonus of their own Circles
_burn(msg.sender, toTokenId(msg.sender), 2 * WELCOME_BONUS);
if (block.timestamp > invitationOnlyTime) {
// after the bootstrap period, the inviter must burn the invitation cost
_burn(msg.sender, toTokenId(msg.sender), INVITATION_COST);

// invited receives the welcome bonus in their personal Circles
_mint(_human, toTokenId(_human), WELCOME_BONUS, "");
// invited receives the welcome bonus in their personal Circles
_mint(_human, toTokenId(_human), WELCOME_BONUS, "");
}

// set trust to indefinite future, but avatar can edit this later
_trust(msg.sender, _human, INDEFINITE_FUTURE);
Expand Down Expand Up @@ -387,6 +411,38 @@ contract Hub is Circles, IHubV2 {
return (mintTime.lastMintTime == INDEFINITE_FUTURE);
}

/**
* @notice Migrate allows to migrate v1 Circles to v2 Circles. During bootstrap period,
* no invitation cost needs to be paid for new humans to be registered. After the bootstrap
* period the same invitation cost applies as for normal invitations, and this requires the
* owner to be a human and to have enough personal Circles to pay the invitation cost.
* Organizations and groups have to ensure all humans have been registered after the bootstrap period.
* Can only be called by the migration contract.
* @param _owner address of the owner of the v1 Circles and beneficiary of the v2 Circles
* @param _avatars array of avatar addresses to migrate
* @param _amounts array of amounts in inflationary v1 units to migrate
*/
function migrate(address _owner, address[] calldata _avatars, uint256[] calldata _amounts) external onlyMigration {
require(avatars[_owner] != address(0), "Only registered avatars can migrate v1 tokens.");
require(_avatars.length == _amounts.length, "Arrays must have the same length.");

// register all unregistered avatars as humans, and check that registered avatars are humans
// after the bootstrap period, the _owner needs to pay the equivalent invitation cost for all newly registered humans
uint256 cost = INVITATION_COST * _ensureAvatarsRegistered(_avatars);

// Invitation cost only applies after the bootstrap period
if (block.timestamp > invitationOnlyTime && cost > 0) {
// personal Circles are required to burn the invitation cost
require(isHuman(_owner), "Only humans can migrate v1 tokens after the bootstrap period.");
_burn(_owner, toTokenId(_owner), cost);
}

for (uint256 i = 0; i < _avatars.length; i++) {
// mint the migrated balances to _owner
_mint(_owner, toTokenId(_avatars[i]), _amounts[i], "");
}
}

/**
* @notice Burn allows to burn Circles owned by the caller.
* @param _id Circles identifier of the Circles to burn
Expand Down Expand Up @@ -670,6 +726,20 @@ contract Hub is Circles, IHubV2 {
emit Trust(_truster, _trustee, _expiry);
}

function _ensureAvatarsRegistered(address[] calldata _avatars) internal returns (uint256) {
uint256 registrationCount = 0;
for (uint256 i = 0; i < _avatars.length; i++) {
if (avatars[_avatars[i]] == address(0)) {
registrationCount++;
_registerHuman(_avatars[i]);
} else {
require(isHuman(_avatars[i]), "Only humans can be registered.");
}
}

return registrationCount;
}

/**
* Checks the status of an avatar's Circles in the Hub v1 contract,
* and returns the address of the Circles if it exists and is not stopped.
Expand Down
1 change: 1 addition & 0 deletions src/hub/IHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";

interface IHubV2 is IERC1155 {
function avatars(address avatar) external view returns (address);
function migrate(address owner, address[] calldata avatars, uint256[] calldata amounts) external;
function mintPolicies(address avatar) external view returns (address);
function burn(uint256 id, uint256 amount, bytes calldata data) external;
}
112 changes: 45 additions & 67 deletions src/migration/Migration.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,32 @@ pragma solidity >=0.8.13;

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

contract CirclesMigration {
contract Migration {
// Constant

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

// State variables

/**
* @dev The address of the v1 hub contract.
*/
IHubV1 public immutable hubV1;

uint256 public immutable inflation;
uint256 public immutable divisor;
/**
* @dev Deployment timestamp of Hub v1 contract
*/
uint256 public immutable deployedAt;
uint256 public immutable initialIssuance;

/**
* @dev Inflationary period of Hub v1 contract
*/
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.");

Expand All @@ -40,77 +42,53 @@ contract CirclesMigration {
// 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)
/**
* @notice Migrates the given amounts of v1 Circles to v2 Circles.
* @param _avatars The avatars to migrate.
* @param _amounts The amounts in inflationary v1 units to migrate.
* @param _hubV2 The v2 hub contract, given as a constant by the SDK.
* @return convertedAmounts The converted amounts of v2 Circles.
*/
function migrate(address[] calldata _avatars, uint256[] calldata _amounts, IHubV2 _hubV2)
external
returns (uint256 mintedAmount_)
returns (uint256[] memory)
{
uint256 balance = _originCircle.balanceOf(msg.sender);
return mintedAmount_ = convertAndMigrateCircles(_originCircle, balance, _destinationGraph);
// note: _hubV2 is passed in as a parameter to avoid a circular dependency, and minimise code complexity

require(_avatars.length == _amounts.length, "Arrays length mismatch.");

uint256[] memory convertedAmounts = new uint256[](_avatars.length);

for (uint256 i = 0; i < _avatars.length; i++) {
ITokenV1 circlesV1 = ITokenV1(hubV1.userToToken(_avatars[i]));
require(address(circlesV1) != address(0), "Invalid avatar.");
convertedAmounts[i] = convertFromV1ToDemurrage(_amounts[i]);
// transfer the v1 Circles to this contract to be locked
circlesV1.transferFrom(msg.sender, address(this), _amounts[i]);
}

// mint the converted amount of v2 Circles
_hubV2.migrate(msg.sender, _avatars, convertedAmounts);

return convertedAmounts;
}

// 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.
* @notice Converts an amount of v1 Circles to demurrage Circles.
* @param _amount The amount of v1 Circles to convert.
*/
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_) {
function convertFromV1ToDemurrage(uint256 _amount) public view returns (uint256) {
// implement the linear interpolation that was used in V1 UI
uint256 currentPeriod = hubV1.periods();
uint256 nextPeriod = currentPeriod + 1;

// calculate the start of the current period in unix time
uint256 startOfPeriod = deployedAt + currentPeriod * period;

// number of seconds into the new period
Expand Down Expand Up @@ -140,6 +118,6 @@ contract CirclesMigration {
// 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;
return (_amount * 3 * ACCURACY * period) / rP;
}
}
6 changes: 3 additions & 3 deletions test/migration/Migration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ contract GraphTest is Test {

MockHubV1 public mockHubV1;

CirclesMigration public migration;
Migration public migration;

function setUp() public {
mockHubV1 = new MockHubV1();

migration = new CirclesMigration(mockHubV1);
migration = new Migration(mockHubV1);

vm.warp(MOMENT_IN_TIME);
}
Expand All @@ -40,7 +40,7 @@ contract GraphTest is Test {
// possibly even on-chain

// for now require accuracy < 1%
uint256 convertedAmount = migration.convertFromV1ToTimeCircles(originalAmountV1);
uint256 convertedAmount = migration.convertFromV1ToDemurrage(originalAmountV1);
uint256 difference = uint256(0);
if (convertedAmount < expectedAmountV2) {
difference = ACCURACY_ONE - (ACCURACY_ONE * convertedAmount) / expectedAmountV2;
Expand Down
Loading