Skip to content

Commit

Permalink
Merge pull request #94 from CirclesUBI/20240211-claimissuance
Browse files Browse the repository at this point in the history
personalMint and _claimIssuance
  • Loading branch information
jaensen authored Feb 12, 2024
2 parents 42fb4ab + 6ffd3e6 commit d1e9cd1
Show file tree
Hide file tree
Showing 3 changed files with 381 additions and 79 deletions.
111 changes: 111 additions & 0 deletions specifications/TCIP009-demurrage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Constant mint under demurrage

## Mint 1 CRC per hour, always

In demurraged units, Circles mints one CRC per hour per person, every day - maximally, as people need to
show up to mint.

We can rewrite for successive days this first constraint with help of a yet unknown function `D(i)`,
we'll call the global demurrage function.
`D(i)` takes values over `i` integer numbers from zero to arbitrary positive N, where `i` is the i-th day
since day zero. We define `D(0)` to be equal to `1`.

If we assume that `D` is a strict monotonic increasing function, and as a consequence also never
becomes zero, we can write for the (demurraged) mint each day `i`:

day 0: D(0)/D(0) CRC/hr
day 1: D(1)/D(1) CRC/hr
...
day N: D(N)/D(N) CRC/hr

which is by construction, trivially, 1 CRC/hr each day, our first constraint.

Then we can **define** the "inflationary" mint as:

day 0: D(0) CRC/hr
day 1: D(1) CRC/hr
...
day N: D(N) CRC/hr

and accordingly we define the demurraged balance on day `i`, given an inflationary amount as:

B(i) = balance(i) = inflationary_balance / D(i)

We note that this definition of the "demurraged balance" function is linear in sums of the inflationary
balance, so all mints, Circles received and Circles spent are linear under the demurraged balance function.
We can then, without loss of generality, consider a single balance amount, as all operations are
linear combinations on the inflationary amounts.

## Determining `D(i)`

Our second constraint is that the demurraged balances have a 7% per annum demurrage, if it is accounted for
on a yearly basis.

However, we want to correct for the demurrage on a daily basis. To adhere to conventional notations we will
write the conversion out twice, once as standard percentages, and once as a reduction factor, but simply to
pendantically show they are saying the same thing.

All balances are understood as demurraged balances (as that is our constraint), and denoted with B(time). We denote 7% p.a. demurrage as γ'.

After one year, our balance is corrected for 7% or γ':

B(1 yr) = (1 - γ') B(0 yr)

and the same formula, but if we would adjust the demurrage daily, what would the equivalent demurrage rate be?
We can call this unknown demurrage rate Γ' and write for `N=365.25` (days in a year):

B(N days) = (1 - Γ'/N)^N B(0 days)

and we know that the balances in both equations are equal (as we're only rewriting the time unit), so

Γ' = N(1 - (1-γ')^(1/N))

or an equivalent demurrage rate of 7,26% per annum on a daily accounted basis.

For our purposes we don't need to know the percentage though, we simply need to determine `D(i)`.
If we call `γ = 1 - γ'`, and `Γ = 1- Γ'/N`, then we can rewrite the above equations as

B(1 yr) = γ B(0 yr)

and

B(N days) = Γ^N B(0 days)

and directly see that

Γ = γ^(1/N) = 0.99980133200859895743...

Now we have a formula for the demurraged balances expressed in days:

B(i + d) = Γ^i B(d)

for any number `d` and `i` days. Again without loss of generality we can proceed with `d=0`
and write this equation for `i=1, i=2, ...` and remember that
- `B(i) = inflationary_balance / D(i)`
- and this was a linear function, so considering a constant inflationary amount is sufficient,
as any additional mints, or sending and receiving transfers over time can be written as a sum
over which the same argument holds.

We write:

1/D(1) = Γ^1 1/D(0)
1/D(2) = Γ^2 1/D(0)
...
1/D(n) = Γ^n 1/D(0)
1/D(n+1) = Γ^(n+1) 1/D(0)

We already defined `D(0) = 1`, and see that `D(n+1) = (1/Γ) D(n)`, so by induction we conclude
that the global demurrage function `D(i)` is

D(i) = (1/Γ)^i

## Conclusion

So we can conclude that if we substitute this in our definition of "inflationary mint"
then on day `i` the protocol should mint as inflationary amounts

(1/Γ)^i CRC/hour

and the demurraged balance function can adjust these inflationary amounts as

B(i) = Γ^i inflationary_balance
226 changes: 220 additions & 6 deletions src/circles/Circles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,243 @@ pragma solidity >=0.8.13;

import "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol";
import "openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import "../lib/Math64x64.sol";

contract Circles is ERC1155 {
// Constants
// Type declarations

/**
* @dev ERC1155 tokens MUST be 18 decimals. Used to calculate the issuance rate.
* @notice MintTime struct stores the last mint time,
* and the status of a connected v1 Circles contract.
* @dev This is used to store the last mint time for each avatar,
* and the address is used as a status for the connected v1 Circles contract.
* The address is kept at zero address if the avatar is not registered in Hub v1.
* If the avatar is registered in Hub v1, but the associated Circles ERC20 contract
* has not been stopped, then the address is set to that v1 Circles contract address.
* Once the Circles v1 contract has been stopped, the address is set to 0x01.
* At every observed transition of the status of the v1 Circles contract,
* the lastMintTime will be updated to the current timestamp to avoid possible
* overlap of the mint between Hub v1 and Hub v2.
*/
uint8 public constant DECIMALS = uint8(18);
struct MintTime {
address mintV1Status;
uint96 lastMintTime;
}

// Constants

/**
* @notice Issue one Circle per hour for each human.
* @notice Issue one Circle per hour for each human in demurraged units.
* So per second issue 10**18 / 3600 = 277777777777778 attoCircles.
*/
uint256 public constant ISSUANCE_PERIOD = 1 hours;
uint256 public constant ISSUANCE_PER_SECOND = uint256(277777777777778);

/**
* @notice Upon claiming, the maximum claim is upto two weeks
* of history. Unclaimed older Circles are unclaimable.
*/
uint256 public constant MAX_CLAIM_DURATION = 2 weeks;

/**
* @notice Demurrage window reduces the resolution for calculating
* the demurrage of balances from once per second (block.timestamp)
* to once per day.
*/
uint256 public constant DEMURRAGE_WINDOW = 1 days;

/**
* @notice Reduction factor GAMMA for applying demurrage to balances
* demurrage_balance(d) = GAMMA^d * inflationary_balance
* where 'd' is expressed in days (DEMURRAGE_WINDOW) since demurrage_day_zero,
* and GAMMA < 1.
* GAMMA_64x64 stores the numerator for the signed 128bit 64.64
* fixed decimal point expression:
* GAMMA = GAMMA_64x64 / 2**64.
* To obtain GAMMA for a daily accounting of 7% p.a. demurrage
* => GAMMA = (0.93)^(1/365.25)
* = 0.99980133200859895743...
* and expressed in 64.64 fixed point representation:
* => GAMMA_64x64 = 18443079296116538654
* For more details, see ./specifications/TCIP009-demurrage.md
*/
int128 public constant GAMMA_64x64 = int128(18443079296116538654);

/**
* @notice For calculating the inflationary mint amount on day `d`
* since demurrage_day_zero, a person can mint
* (1/GAMMA)^d CRC / hour
* As GAMMA is a constant, to save gas costs store the inverse
* as BETA = 1 / GAMMA.
* BETA_64x64 is the 64.64 fixed point representation:
* BETA_64x64 = 2**64 / ((0.93)^(1/365.25))
* = 18450409579521241655
* For more details, see ./specifications/TCIP009-demurrage.md
*/
int128 public constant BETA_64x64 = int128(18450409579521241655);

/**
* @dev Address used to indicate that the associated v1 Circles contract has been stopped.
*/
address public constant CIRCLES_STOPPED_V1 = address(0x1);

/**
* @notice Indefinite future, or approximated with uint96.max
*/
uint96 public constant INDEFINITE_FUTURE = type(uint96).max;

/**
* @dev ERC1155 tokens MUST be 18 decimals.
*/
uint8 public constant DECIMALS = uint8(18);

/**
* @dev EXA factor as 10^18
*/
uint256 internal constant EXA = uint256(10 ** DECIMALS);

/**
* Store the signed 128-bit 64.64 representation of 1 as a constant
*/
int128 internal constant ONE_64x64 = int128(2 ** 64);

// State variables

/**
* @notice Demurrage day zero stores the start of the global demurrage curve
* As Circles Hub v1 was deployed on Thursday 15th October 2020 at 6:25:30 pm UTC,
* or 1602786330 unix time, in production this value MUST be set to 1602720000 unix time,
* or midnight prior of the same day of deployment, marking the start of the first day
* where there was no inflation on one CRC per hour.
*/
uint256 public immutable demurrage_day_zero;

/**
* @notice The mapping of avatar addresses to the last mint time,
* and the status of the v1 Circles minting.
* @dev This is used to store the last mint time for each avatar.
*/
mapping(address => MintTime) public mintTimes;

// Constructor

constructor(string memory uri_) ERC1155(uri_) {}
constructor(uint256 _demurrage_day_zero, string memory _uri) ERC1155(_uri) {
demurrage_day_zero = _demurrage_day_zero;
}

// External functions

// Public functions

/**
* @notice Calculate the issuance for a human's avatar.
* @param _human Address of the human's avatar to calculate the issuance for.
*/
function calculateIssuance(address _human) public view returns (uint256) {
MintTime storage mintTime = mintTimes[_human];
require(
mintTime.mintV1Status == address(0) || mintTime.mintV1Status == CIRCLES_STOPPED_V1,
"Circles v1 contract cannot be active."
);

if (uint256(mintTime.lastMintTime) + 1 hours >= block.timestamp) {
// Mint time is set to indefinite future for stopped mints in v2
// and wait at least one hour for a minimal mint issuance
return 0;
}

// calculate the start of the claimable period
uint256 startMint = _max(block.timestamp - MAX_CLAIM_DURATION, mintTime.lastMintTime);

// day of start of mint, dA
uint256 dA = _day(startMint);
// day of end of mint (now), dB
uint256 dB = _day(block.timestamp);

// todo: later cache these computations, as they roll through a window of 15 days/values
// because there is a max claimable window, and once filled, only once per day we need to calculate
// a new value in the cache for all mints.

// iA = Beta^dA
int128 iA = Math64x64.pow(BETA_64x64, dA);
// iB = Beta^dB
int128 iB = 0;
if (dA == dB) {
// if the start and end day are the same, then the issuance factor is the same
iB = iA;
} else {
iB = Math64x64.pow(BETA_64x64, dB);
}
uint256 fullIssuance = 0;
{
// for the geometric sum we need Beta^(dB + 1) = iB1
int128 iB1 = Math64x64.mul(iB, BETA_64x64);

// first calculate the full issuance over the complete days [dA, dB]
// using the geometric sum:
// SUM_i=dA..dB (Beta^i) = (Beta^(dB + 1) - 1) / (Beta^dA - 1)
int128 term1 = iB1.sub(ONE_64x64);
int128 term2 = iA.sub(ONE_64x64);
int128 geometricSum = Math64x64.div(term1, term2);
// 24 hours * 1 CRC/hour * EXA * geometricSum
fullIssuance = Math64x64.mulu(geometricSum, 24 * EXA);
}

// But now we overcounted, as we start day A at startMint
// and end day B at block.timestamp, so we need to adjust
uint256 overcountA = startMint - (dA * 1 days + demurrage_day_zero);
uint256 overcountB = (dB + 1) * 1 days + demurrage_day_zero - block.timestamp;

uint256 overIssuanceA = Math64x64.mulu(iA, overcountA * ISSUANCE_PER_SECOND);
uint256 overIssuanceB = Math64x64.mulu(iB, overcountB * ISSUANCE_PER_SECOND);

// subtract the overcounted issuance
uint256 issuance = fullIssuance - overIssuanceA - overIssuanceB;

return issuance;
}

// Internal functions

/**
* @notice Claim issuance for a human's avatar and update the last mint time.
* @param _human Address of the human's avatar to claim the issuance for.
*/
function _claimIssuance(address _human) internal {
uint256 issuance = calculateIssuance(_human);
require(issuance > 0, "No issuance to claim.");
// mint personal Circles to the human
_mint(_human, _toTokenId(_human), issuance, "");

// update the last mint time
mintTimes[_human].lastMintTime = uint96(block.timestamp);
}

/**
* @dev Calculate the day since demurrage_day_zero for a given timestamp.
* @param _timestamp Timestamp for which to calculate the day since
* demurrage_day_zero.
*/
function _day(uint256 _timestamp) internal view returns (uint256) {
// calculate which day the timestamp is in, rounding down
return (_timestamp - demurrage_day_zero) / DEMURRAGE_WINDOW;
}

/**
* @dev Casts an avatar address to a tokenId uint256.
* @param _avatar avatar address to convert to tokenId
*/
function _toTokenId(address _avatar) internal pure returns (uint256) {
return uint256(uint160(_avatar));
}

// Private functions

/**
* @dev Max function to compare two values.
* @param a Value a
* @param b Value b
*/
function _max(uint256 a, uint256 b) private pure returns (uint256) {
return a >= b ? a : b;
}
}
Loading

0 comments on commit d1e9cd1

Please sign in to comment.