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

V0.3.7 patch21 extra tests around demurrage #70

Merged
merged 3 commits into from
Oct 11, 2024
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
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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicates of public convertDemurrageToInflationaryValue

// 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
Loading