Skip to content

Commit

Permalink
Merge pull request #70 from aboutcircles/v0.3.7-patch21-extra-tests-a…
Browse files Browse the repository at this point in the history
…round-demurrage

V0.3.7 patch21 extra tests around demurrage
  • Loading branch information
benjaminbollen authored Oct 11, 2024
2 parents 82602a2 + 6701cf1 commit 1337093
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 71 deletions.
13 changes: 0 additions & 13 deletions src/circles/Demurrage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,4 @@ contract Demurrage is ICirclesCompactErrors, ICirclesDemurrageErrors {
// and do not cache it
return Math64x64.pow(GAMMA_64x64, _dayDifference);
}

/**
* Calculate the inflationary balance of a demurraged balance
* @param _balance Demurraged balance to calculate the inflationary balance of
* @param _dayUpdated The day the balance was last updated
*/
function _calculateInflationaryBalance(uint256 _balance, uint256 _dayUpdated) internal pure returns (uint256) {
// calculate the inflationary balance by dividing the balance by GAMMA^days
// note: GAMMA < 1, so dividing by a power of it, returns a bigger number,
// so the numerical imprecision is in the least significant bits.
int128 i = Math64x64.pow(BETA_64x64, _dayUpdated);
return Math64x64.mulu(i, _balance);
}
}
10 changes: 0 additions & 10 deletions src/circles/DiscountedBalances.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,6 @@ contract DiscountedBalances is Demurrage {

// Internal functions

/**
* @dev Calculate the inflationary balance of a discounted balance
* @param _account Address of the account to calculate the balance of
* @param _id Circles identifier for which to calculate the balance
*/
function _inflationaryBalanceOf(address _account, uint256 _id) internal view returns (uint256) {
DiscountedBalance memory discountedBalance = discountedBalances[_id][_account];
return _calculateInflationaryBalance(discountedBalance.balance, discountedBalance.lastUpdatedDay);
}

/**
* @dev Update the balance of an account for a given Circles identifier
* @param _account Address of the account to update the balance of
Expand Down
2 changes: 1 addition & 1 deletion src/circles/InflationaryOperator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,6 @@ contract InflationaryCirclesOperator is BatchedDemurrage {
function _inflationaryBalanceOf(address _account, uint256 _id) internal view returns (uint256) {
// retrieve the balance in demurrage units (of today)
uint256 balance = hub.balanceOf(_account, _id);
return _calculateInflationaryBalance(balance, day(block.timestamp));
return convertDemurrageToInflationaryValue(balance, day(block.timestamp));
}
}
4 changes: 2 additions & 2 deletions src/lift/DemurrageCircles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ contract DemurrageCircles is MasterCopyNonUpgradable, ERC20DiscountedBalances, E
_burn(msg.sender, _amount);
hub.safeTransferFrom(address(this), msg.sender, toTokenId(avatar), _amount, "");

uint256 inflationaryAmount = _calculateInflationaryBalance(_amount, day(block.timestamp));
uint256 inflationaryAmount = convertDemurrageToInflationaryValue(_amount, day(block.timestamp));

emit WithdrawDemurraged(msg.sender, _amount, inflationaryAmount);
}
Expand Down Expand Up @@ -107,7 +107,7 @@ contract DemurrageCircles is MasterCopyNonUpgradable, ERC20DiscountedBalances, E
if (_id != toTokenId(avatar)) revert CirclesInvalidCirclesId(_id, 0);
_mint(_from, _amount);

uint256 inflationaryAmount = _calculateInflationaryBalance(_amount, day(block.timestamp));
uint256 inflationaryAmount = convertDemurrageToInflationaryValue(_amount, day(block.timestamp));

emit DepositDemurraged(_from, _amount, inflationaryAmount);

Expand Down
5 changes: 0 additions & 5 deletions src/lift/ERC20DiscountedBalances.sol
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,6 @@ contract ERC20DiscountedBalances is ERC20Permit, BatchedDemurrage, IERC20 {

// Internal functions

function _inflationaryBalanceOf(address _account) internal view returns (uint256) {
DiscountedBalance memory discountedBalance = discountedBalances[_account];
return _calculateInflationaryBalance(discountedBalance.balance, discountedBalance.lastUpdatedDay);
}

function _updateBalance(address _account, uint256 _balance, uint64 _day) internal {
if (_balance > MAX_VALUE) {
// Balance exceeds maximum value.
Expand Down
56 changes: 20 additions & 36 deletions src/lift/ERC20InflationaryBalances.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ import "../circles/BatchedDemurrage.sol";
import "./ERC20Permit.sol";

contract ERC20InflationaryBalances is ERC20Permit, BatchedDemurrage, IERC20 {
// Constants

uint8 internal constant EXTENDED_ACCURACY_BITS = 64;

// State variables

uint256 internal _extendedTotalSupply;
uint256 internal _totalSupply;

mapping(address => uint256) private _extendedAccuracyBalances;
mapping(address => uint256) private _balances;

// Constructor

Expand Down Expand Up @@ -55,66 +51,54 @@ contract ERC20InflationaryBalances is ERC20Permit, BatchedDemurrage, IERC20 {
}

function balanceOf(address _account) external view returns (uint256) {
return _extendedAccuracyBalances[_account] >> EXTENDED_ACCURACY_BITS;
return _balances[_account];
}

function allowance(address _owner, address _spender) external view returns (uint256) {
return _allowances[_owner][_spender];
}

function totalSupply() external view returns (uint256) {
return _extendedTotalSupply >> EXTENDED_ACCURACY_BITS;
return _totalSupply;
}

// Internal functions

function _convertToExtended(uint256 _amount) internal pure returns (uint256) {
if (_amount > MAX_VALUE) revert CirclesAmountOverflow(_amount, 0);
return _amount << EXTENDED_ACCURACY_BITS;
}

function _transfer(address _from, address _to, uint256 _amount) internal {
uint256 extendedAmount = _convertToExtended(_amount);
uint256 extendedFromBalance = _extendedAccuracyBalances[_from];
if (extendedFromBalance < extendedAmount) {
revert ERC20InsufficientBalance(_from, extendedFromBalance >> EXTENDED_ACCURACY_BITS, _amount);
uint256 fromBalance = _balances[_from];
if (fromBalance < _amount) {
revert ERC20InsufficientBalance(_from, fromBalance, _amount);
}
unchecked {
_extendedAccuracyBalances[_from] = extendedFromBalance - extendedAmount;
_balances[_from] = fromBalance - _amount;
// rely on total supply not having overflowed
_extendedAccuracyBalances[_to] += extendedAmount;
_balances[_to] += _amount;
}
emit Transfer(_from, _to, _amount);
}

function _mintFromDemurragedAmount(address _owner, uint256 _demurragedAmount) internal returns (uint256) {
// first convert to extended accuracy representation so we have extra garbage bits,
// before we apply the inflation factor, which will produce errors in the least significant bits
uint256 extendedAmount =
_calculateInflationaryBalance(_convertToExtended(_demurragedAmount), day(block.timestamp));
uint256 inflationaryAmount = convertDemurrageToInflationaryValue(_demurragedAmount, day(block.timestamp));
// here ensure total supply does not overflow
_extendedTotalSupply += extendedAmount;
_totalSupply += inflationaryAmount;
unchecked {
_extendedAccuracyBalances[_owner] += extendedAmount;
_balances[_owner] += inflationaryAmount;
}
emit Transfer(address(0), _owner, extendedAmount >> EXTENDED_ACCURACY_BITS);
emit Transfer(address(0), _owner, inflationaryAmount);

return extendedAmount >> EXTENDED_ACCURACY_BITS;
return inflationaryAmount;
}

function _burn(address _owner, uint256 _amount) internal returns (uint256) {
uint256 extendedAmount = _convertToExtended(_amount);
uint256 extendedOwnerBalance = _extendedAccuracyBalances[_owner];
if (extendedOwnerBalance < extendedAmount) {
revert ERC20InsufficientBalance(_owner, _extendedAccuracyBalances[_owner], _amount);
function _burn(address _owner, uint256 _amount) internal {
uint256 ownerBalance = _balances[_owner];
if (ownerBalance < _amount) {
revert ERC20InsufficientBalance(_owner, ownerBalance, _amount);
}
unchecked {
_extendedAccuracyBalances[_owner] = extendedOwnerBalance - extendedAmount;
_balances[_owner] = ownerBalance - _amount;
// rely on total supply tracking complete sum of balances
_extendedTotalSupply -= extendedAmount;
_totalSupply -= _amount;
}
emit Transfer(_owner, address(0), _amount);

return extendedAmount;
}
}
5 changes: 2 additions & 3 deletions src/lift/InflationaryCircles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,10 @@ contract InflationaryCircles is MasterCopyNonUpgradable, ERC20InflationaryBalanc
// External functions

function unwrap(uint256 _amount) external {
uint256 extendedAmount = _burn(msg.sender, _amount);
_burn(msg.sender, _amount);
// calculate demurraged amount in extended accuracy representation
// then discard garbage bits by shifting right
uint256 demurragedAmount =
convertInflationaryToDemurrageValue(extendedAmount, day(block.timestamp)) >> EXTENDED_ACCURACY_BITS;
uint256 demurragedAmount = convertInflationaryToDemurrageValue(_amount, day(block.timestamp));

hub.safeTransferFrom(address(this), msg.sender, toTokenId(avatar), demurragedAmount, "");

Expand Down
122 changes: 122 additions & 0 deletions test/circles/Demurrage.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ contract DemurrageTest is Test, TimeCirclesSetup, Approximation {
startTime();

demurrage = new MockDemurrage();

demurrage.setInflationDayZero(INFLATION_DAY_ZERO);
}

// Tests
Expand All @@ -55,4 +57,124 @@ contract DemurrageTest is Test, TimeCirclesSetup, Approximation {
);
}
}

// Test the inversion accuracy of the gamma and beta exponentiation over 20 and 100 years
// with and without the extension
// conclusion: we can just drop the extension as the 64x64 fixed point is accurate, and unsure if the extension
// is actually doing something? -- maybe not because GAMMA and BETA have a fixed precision, so they will always
// introduce errors at their precision level... so the extension is pointless and costs extra gas

// (note) leaving these tests here for now, to document why the extension is removed from Inflationary ERC20 in patch21!
// this can later be tidied up and removed

function testInversionGammaBeta64x64_20years() public {
// for the coming 20 years (2024 is year 4 since INFLATION_DAY_ZERO)
// check that simply exponentiating the number of days remains accurate
// and without overflow

// one year in unix time (approximately)
uint256 oneYear = 365 * 24 * 3600;

for (uint256 i = 0; i <= 20; i++) {
uint256 secondsNow = INFLATION_DAY_ZERO + i * oneYear;
uint64 dayCount = demurrage.day(secondsNow);
// convert one CRC to inflationary value
uint256 inflationaryOneCRC = demurrage.convertDemurrageToInflationaryValue(100 * CRC, dayCount);
// now invert the operation
uint256 demurrageOneCRC = demurrage.convertInflationaryToDemurrageValue(inflationaryOneCRC, dayCount);
assertTrue(relativeApproximatelyEqual(100 * CRC, demurrageOneCRC, 1000 * DUST));
console.log("year ", i, ": ", demurrageOneCRC);
}
}

function testInversionGammaBeta64x64_100years() public {
// for the coming 100 years (2024 is year 4 since INFLATION_DAY_ZERO)
// check that simply exponentiating the number of days remains accurate
// and without overflow

// one year in unix time (approximately)
uint256 oneYear = 365 * 24 * 3600;

for (uint256 i = 0; i <= 100; i++) {
uint256 secondsNow = INFLATION_DAY_ZERO + i * oneYear;
uint64 dayCount = demurrage.day(secondsNow);
// convert one CRC to inflationary value
uint256 inflationaryOneCRC = demurrage.convertDemurrageToInflationaryValue(CRC, dayCount);
// now invert the operation
uint256 demurrageOneCRC = demurrage.convertInflationaryToDemurrageValue(inflationaryOneCRC, dayCount);
assertTrue(relativeApproximatelyEqual(CRC, demurrageOneCRC, 1000 * DUST));
}
}

function testInversionGammaBeta64x64_100years_withExtension() public {
// for the coming 100 years (2024 is year 4 since INFLATION_DAY_ZERO)
// check that simply exponentiating the number of days remains accurate
// and without overflow

// one year in unix time (approximately)
uint256 oneYear = 365 * 24 * 3600;

uint8 accuracy_shift = 64;

uint192 amount = uint192(10000000 * CRC);
console.log("amount: ", amount);

for (uint256 i = 0; i <= 100; i++) {
uint256 secondsNow = INFLATION_DAY_ZERO + i * oneYear;
uint64 dayCount = demurrage.day(secondsNow);
// convert one CRC to inflationary value
uint256 extendedAmount = amount << accuracy_shift;
uint256 inflationaryAmountExtended = demurrage.convertDemurrageToInflationaryValue(extendedAmount, dayCount);
uint256 trimmedInflationaryAmount = inflationaryAmountExtended >> accuracy_shift;
// now invert the operation
uint256 extendedInflationAmountTrimmed = trimmedInflationaryAmount << accuracy_shift;
uint256 demurrageAmountExtended =
demurrage.convertInflationaryToDemurrageValue(extendedInflationAmountTrimmed, dayCount);
uint256 trimmedDemurrageAmount = demurrageAmountExtended >> accuracy_shift;
assertTrue(relativeApproximatelyEqual(amount, trimmedDemurrageAmount, 1000 * DUST));
console.log("year ", i, ": ", trimmedDemurrageAmount);
}
}

function testInversionGammaBeta64x64_100years_withExtension_comparison() public {
// for the coming 100 years (2024 is year 4 since INFLATION_DAY_ZERO)
// check that simply exponentiating the number of days remains accurate
// and without overflow

// one year in unix time (approximately)
uint256 oneYear = 365 * 24 * 3600;

uint8 accuracy_shift = 64;

uint192 amount = uint192(254516523121 * CRC);
console.log("amount: ", amount);

for (uint256 i = 0; i <= 100; i++) {
uint256 secondsNow = INFLATION_DAY_ZERO + i * oneYear;
uint64 dayCount = demurrage.day(secondsNow);
// convert one CRC to inflationary value
uint256 extendedAmount = amount << accuracy_shift;
uint256 inflationaryAmountExtended = demurrage.convertDemurrageToInflationaryValue(extendedAmount, dayCount);
uint256 trimmedInflationaryAmount = inflationaryAmountExtended >> accuracy_shift;
// now invert the operation
uint256 extendedInflationAmountTrimmed = trimmedInflationaryAmount << accuracy_shift;
uint256 demurrageAmountExtended =
demurrage.convertInflationaryToDemurrageValue(extendedInflationAmountTrimmed, dayCount);
uint256 trimmedDemurrageAmount = demurrageAmountExtended >> accuracy_shift;

// now do the same without extension
uint256 inflationaryAmount_withoutExtension =
demurrage.convertDemurrageToInflationaryValue(amount, dayCount);
// now invert the operation
uint256 demurrageAmount_withoutExtension =
demurrage.convertInflationaryToDemurrageValue(inflationaryAmount_withoutExtension, dayCount);

uint256 diff = demurrageAmount_withoutExtension > trimmedDemurrageAmount
? demurrageAmount_withoutExtension - trimmedDemurrageAmount
: trimmedDemurrageAmount - demurrageAmount_withoutExtension;

assertTrue(diff == 0);
console.log(trimmedDemurrageAmount, " vs ", demurrageAmount_withoutExtension);
}
}
}
4 changes: 4 additions & 0 deletions test/circles/MockDemurrage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ contract MockDemurrage is Demurrage {
return GAMMA_64x64;
}

function beta_64x64() external pure returns (int128) {
return BETA_64x64;
}

function r(uint256 _i) external view returns (int128) {
return R[_i];
}
Expand Down
File renamed without changes.
Loading

0 comments on commit 1337093

Please sign in to comment.