Skip to content

Commit

Permalink
(migration): redo migration for hub v2
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminbollen committed Mar 1, 2024
1 parent 0200e48 commit c696aaf
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 113 deletions.
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
71 changes: 66 additions & 5 deletions src/hub/Hub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ contract Hub is Circles {
// 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 @@ -59,6 +64,11 @@ contract Hub is Circles {
*/
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 @@ -122,7 +132,7 @@ contract Hub is Circles {
* 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.");
_;
}

Expand All @@ -141,6 +151,7 @@ contract Hub is Circles {
*/
constructor(
IHubV1 _hubV1,
address _migration,
uint256 _inflation_day_zero,
address _standardTreasury,
uint256 _bootstrapTime,
Expand All @@ -155,6 +166,9 @@ contract Hub is Circles {
// 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 @@ -196,8 +210,10 @@ contract Hub is Circles {
// 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, "");
Expand Down Expand Up @@ -370,6 +386,37 @@ contract Hub is Circles {
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.
* @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 {
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], "");
}
}

// check if path transfer can be fully ERC1155 compatible
// note: matrix math needs to consider mints, otherwise it won't add up

Expand Down Expand Up @@ -619,6 +666,20 @@ contract Hub is Circles {
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
3 changes: 2 additions & 1 deletion src/hub/IHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
pragma solidity >=0.8.13;

interface IHubV2 {
function avatars(address _avatar) external view returns (address);
function avatars(address avatar) external view returns (address);
function migrate(address owner, address[] calldata avatars, uint256[] calldata amounts) external;
}
110 changes: 43 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,51 @@ 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);
}

// 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 +116,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;
}
}
Loading

0 comments on commit c696aaf

Please sign in to comment.