Skip to content

Commit

Permalink
Merge pull request #100 from aboutcircles/test/hub-unit-test-coverage
Browse files Browse the repository at this point in the history
(hub-test): implemented DiscountedBalances unit test coverage
  • Loading branch information
benjaminbollen authored Nov 18, 2024
2 parents 91cb016 + 98975b2 commit 653f1ed
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 31 deletions.
68 changes: 37 additions & 31 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
CirclesTest:testCalculateIssuance() (gas: 3729002)
CirclesTest:testConsecutiveClaimablePeriods() (gas: 375082)
CirclesTest:testDemurragedTransfer() (gas: 229055)
CompositeMintGroupsTest:testCompositeGroupMint() (gas: 212459)
DemurrageTest:testDemurrageFactor() (gas: 79139)
DemurrageTest:testFuzzStablePointIssuance(int192) (runs: 256, μ: 63114, ~: 63250)
DemurrageTest:testInversionGammaBeta64x64_100years() (gas: 958541)
DemurrageTest:testInversionGammaBeta64x64_100years_withExtension() (gas: 1083080)
DemurrageTest:testInversionGammaBeta64x64_100years_withExtension_comparison() (gas: 1703713)
DemurrageTest:testInversionGammaBeta64x64_20years() (gas: 221562)
DemurrageTest:testRepeatedDemurrage() (gas: 15264702)
ERC20LiftTest:testERC20Demurrage() (gas: 133270)
ERC20LiftTest:testERC20Wrap() (gas: 954629)
ERC20LiftTest:testWrapAndUnwrapInflationaryERC20() (gas: 89881)
GroupMintTest:testRegisterGroup() (gas: 165153)
HubPathTransferTest:testOperateFlowMatrixConsentedFlow() (gas: 265369)
MigrationTest:testConversionMigrationV1ToTimeCircles() (gas: 18365)
MintGroupCirclesTest:testDirectSelfGroupMintFails() (gas: 341135)
MintGroupCirclesTest:testGroupMint() (gas: 340165)
MintGroupCirclesTest:testGroupMintFail() (gas: 23302)
MintGroupCirclesTest:testGroupMintMany() (gas: 858065)
MintGroupCirclesTest:testGroupMintMultiCollateral() (gas: 755126)
MintGroupCirclesTest:testSequentialGroupMint() (gas: 511760)
NamesTest:testBase58Conversion() (gas: 78904)
NamesTest:testCustomName() (gas: 40809)
NamesTest:testInvalidCustomNames() (gas: 20017)
NamesTest:testMetadataDigest() (gas: 35073)
NamesTest:testShortName() (gas: 102230)
NamesTest:testShortNameWithNonce() (gas: 73360)
NamesTest:testShortNameWithPadding() (gas: 71662)
V1MintStatusUpdateTest:testMigrationFromV1DuringBootstrap() (gas: 2542748)
CirclesTest:testCalculateIssuance() (gas: 5477687)
CirclesTest:testConsecutiveClaimablePeriods() (gas: 489668)
CirclesTest:testDemurragedTransfer() (gas: 267048)
CompositeMintGroupsTest:testCompositeGroupMint() (gas: 234772)
DemurrageTest:testDemurrageFactor() (gas: 89594)
DemurrageTest:testFuzzStablePointIssuance(int192) (runs: 256, μ: 92257, ~: 92418)
DemurrageTest:testInversionGammaBeta64x64_100years() (gas: 1304950)
DemurrageTest:testInversionGammaBeta64x64_100years_withExtension() (gas: 1471985)
DemurrageTest:testInversionGammaBeta64x64_100years_withExtension_comparison() (gas: 2275673)
DemurrageTest:testInversionGammaBeta64x64_20years() (gas: 307602)
DemurrageTest:testRepeatedDemurrage() (gas: 21734392)
DiscountedBalancesTest:testBalanceOfOnDay(address,uint256,uint64) (runs: 257, μ: 50694, ~: 49914)
DiscountedBalancesTest:testDiscountAndAddToBalance() (gas: 96291)
DiscountedBalancesTest:testDiscountFromInitialZeroBalanceAndAddValue(address,uint256,uint256,uint64) (runs: 258, μ: 37247, ~: 39734)
DiscountedBalancesTest:testRevertInputDayBeforeLastUpdatedDay(uint64) (runs: 258, μ: 51247, ~: 51247)
DiscountedBalancesTest:testTotalSupply(uint256,uint192) (runs: 258, μ: 52826, ~: 53576)
DiscountedBalancesTest:testUpdateBalance(address,uint256,uint256,uint64) (runs: 258, μ: 33619, ~: 37452)
ERC20LiftTest:testERC20Demurrage() (gas: 154291)
ERC20LiftTest:testERC20Wrap() (gas: 1019942)
ERC20LiftTest:testWrapAndUnwrapInflationaryERC20() (gas: 107003)
GroupMintTest:testRegisterGroup() (gas: 169388)
HubPathTransferTest:testOperateFlowMatrixConsentedFlow() (gas: 303592)
MigrationTest:testConversionMigrationV1ToTimeCircles() (gas: 27527)
MintGroupCirclesTest:testDirectSelfGroupMintFails() (gas: 371260)
MintGroupCirclesTest:testGroupMint() (gas: 370079)
MintGroupCirclesTest:testGroupMintFail() (gas: 25601)
MintGroupCirclesTest:testGroupMintMany() (gas: 980226)
MintGroupCirclesTest:testGroupMintMultiCollateral() (gas: 853285)
MintGroupCirclesTest:testSequentialGroupMint() (gas: 583887)
NamesTest:testBase58Conversion() (gas: 122264)
NamesTest:testCustomName() (gas: 42499)
NamesTest:testInvalidCustomNames() (gas: 20402)
NamesTest:testMetadataDigest() (gas: 36399)
NamesTest:testShortName() (gas: 128347)
NamesTest:testShortNameWithNonce() (gas: 82799)
NamesTest:testShortNameWithPadding() (gas: 79690)
V1MintStatusUpdateTest:testMigrationFromV1DuringBootstrap() (gas: 3848577)
201 changes: 201 additions & 0 deletions test/circles/DiscountedBalances.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.13;

import {console2, Test} from "forge-std/Test.sol";
import {TimeCirclesSetup} from "../setup/TimeCirclesSetup.sol";
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import {ICirclesCompactErrors, ICirclesDemurrageErrors} from "src/errors/Errors.sol";
import {IDiscountedBalances, MockDiscountedBalances} from "./MockDiscountedBalances.sol";

contract DiscountedBalancesTest is Test, TimeCirclesSetup, ICirclesCompactErrors, ICirclesDemurrageErrors {
MockDiscountedBalances public discountedBalances;
// represents Demurrage.MAX_VALUE
uint256 internal maxBalance;
// default test values
address internal defaultAvatar;
uint256 internal defaultId;
uint192 internal defaultValue;
uint64 internal defaultDay;

function setUp() public {
// Set time in 2021
startTime();

discountedBalances = new MockDiscountedBalances(INFLATION_DAY_ZERO);
maxBalance = discountedBalances.maxBalance();

defaultAvatar = address(0xa4a7a5);
defaultId = uint256(0x1d);
defaultValue = uint192(10 ** 18);
defaultDay = uint64(0xda1);
}

// Internal _updateBalance(address _account, uint256 _id, uint256 _balance, uint64 _day)

function testUpdateBalance(address avatar, uint256 id, uint256 newBalance, uint64 newDay) public {
if (newBalance <= maxBalance) {
// balance should be updated correctly
discountedBalances.updateBalance(avatar, id, newBalance, newDay);

uint192 updatedAvatarBalance = discountedBalances.getAvatarBalanceValue(id, avatar);
uint64 updatedAvatarLastUpdatedDay = discountedBalances.getAvatarLastUpdatedDayValue(id, avatar);

assertEq(updatedAvatarBalance, newBalance);
assertEq(updatedAvatarLastUpdatedDay, newDay);
} else {
// call should revert when balance exceeds MAX_VALUE
vm.expectRevert(abi.encodeWithSelector(CirclesErrorAddressUintArgs.selector, avatar, id, 0x81));
discountedBalances.updateBalance(avatar, id, newBalance, newDay);
}
}

// Internal _discountAndAddToBalance(address _account, uint256 _id, uint256 _value, uint64 _day)

function testDiscountFromInitialZeroBalanceAndAddValue(
address avatar,
uint256 id,
uint256 newBalance,
uint64 newDay
) public {
// behaves exactly the same as _updateBalance
if (newBalance <= maxBalance) {
// balance should be updated correctly
discountedBalances.discountAndAddToBalance(avatar, id, newBalance, newDay);

uint192 updatedAvatarBalance = discountedBalances.getAvatarBalanceValue(id, avatar);
uint64 updatedAvatarLastUpdatedDay = discountedBalances.getAvatarLastUpdatedDayValue(id, avatar);

assertEq(updatedAvatarBalance, newBalance);
assertEq(updatedAvatarLastUpdatedDay, newDay);
} else {
// call should revert when balance exceeds MAX_VALUE
vm.expectRevert(abi.encodeWithSelector(CirclesErrorAddressUintArgs.selector, avatar, id, 0x82));
discountedBalances.discountAndAddToBalance(avatar, id, newBalance, newDay);
}
}

function testDiscountAndAddToBalance() public {
// store default values
discountedBalances.updateBalance(defaultAvatar, defaultId, defaultValue, defaultDay);

// first branch: dayDifference == 0 (input: same day as last updated and defaultValue)
uint64 day = defaultDay;
uint256 addedValue = defaultValue;

discountedBalances.discountAndAddToBalance(defaultAvatar, defaultId, addedValue, day);

// should update balance by adding value without applying discount and leaving same last updated day
uint192 updatedAvatarBalance = discountedBalances.getAvatarBalanceValue(defaultId, defaultAvatar);
uint64 updatedAvatarLastUpdatedDay = discountedBalances.getAvatarLastUpdatedDayValue(defaultId, defaultAvatar);
assertEq(updatedAvatarBalance, 2 * defaultValue);
assertEq(updatedAvatarLastUpdatedDay, defaultDay);

// second branch: dayDifference != 0 and discount < addedValue (input: small day difference and defaultValue)
day += 10;

// should emit DiscountCost events
_expectEmitDiscountEvents(defaultId, defaultAvatar);

discountedBalances.discountAndAddToBalance(defaultAvatar, defaultId, addedValue, day);

// should increase balance by applying small discount and adding significant value, update last updated day
updatedAvatarBalance = discountedBalances.getAvatarBalanceValue(defaultId, defaultAvatar);
updatedAvatarLastUpdatedDay = discountedBalances.getAvatarLastUpdatedDayValue(defaultId, defaultAvatar);
assertTrue(updatedAvatarBalance > 2 * defaultValue && updatedAvatarBalance < 3 * defaultValue);
assertEq(updatedAvatarLastUpdatedDay, day);

// third branch: dayDifference != 0 and discount > addedValue (input: big day difference and small added value)
day += 7200;
addedValue /= 10;

// should emit DiscountCost events
_expectEmitDiscountEvents(defaultId, defaultAvatar);

discountedBalances.discountAndAddToBalance(defaultAvatar, defaultId, addedValue, day);

// should decrease balance by applying significant discount and adding small value, update last updated day
updatedAvatarBalance = discountedBalances.getAvatarBalanceValue(defaultId, defaultAvatar);
updatedAvatarLastUpdatedDay = discountedBalances.getAvatarLastUpdatedDayValue(defaultId, defaultAvatar);
assertTrue(updatedAvatarBalance < defaultValue);
assertEq(updatedAvatarLastUpdatedDay, day);
}

function testRevertInputDayBeforeLastUpdatedDay(uint64 newDay) public {
vm.assume(newDay > 0);
// store default values and newDay (becomes the last updated day)
discountedBalances.updateBalance(defaultAvatar, defaultId, defaultValue, newDay);

// set input as a day before the last updated day
uint64 inputDay = newDay - 1;

// two functions should revert

// test internal discountAndAddToBalance
vm.expectRevert(abi.encodeWithSelector(CirclesErrorAddressUintArgs.selector, defaultAvatar, newDay, 0xA1));
discountedBalances.discountAndAddToBalance(defaultAvatar, defaultId, defaultValue, inputDay);
// test public balanceOfOnDay
vm.expectRevert(abi.encodeWithSelector(CirclesErrorAddressUintArgs.selector, defaultAvatar, newDay, 0xA0));
discountedBalances.balanceOfOnDay(defaultAvatar, defaultId, inputDay);
}

// Public totalSupply(uint256 _id)

function testTotalSupply(uint256 id, uint192 balance) public {
uint64 day = discountedBalances.day(block.timestamp);
discountedBalances.updateTotalSupply(id, balance, day);

uint192 storedSupply = discountedBalances.getTotalSupplyBalanceValue(id);
uint64 storedDay = discountedBalances.getTotalSupplyLastUpdatedDayValue(id);
assertEq(balance, storedSupply);
assertEq(day, storedDay);

uint256 staticCallResult = discountedBalances.totalSupply(id);
assertEq(staticCallResult, balance);

// test total supply is discounted on a new day
if (balance > 10) {
skipTime(1 days);
uint256 discountedTotalSupply = discountedBalances.totalSupply(id);
assertTrue(uint192(discountedTotalSupply) < balance);
// test supply is discounted in a year
skipTime(730 days);
uint256 totalSupplyInAYear = discountedBalances.totalSupply(id);
assertTrue(uint192(totalSupplyInAYear) < uint192(discountedTotalSupply));
}
}

// Public balanceOfOnDay(address _account, uint256 _id, uint64 _day)

function testBalanceOfOnDay(address avatar, uint256 id, uint64 day) public {
vm.assume(day <= type(uint64).max - 31);
// zero balance always returns 0,0
(uint256 balanceOnDay, uint256 discountCost) = discountedBalances.balanceOfOnDay(avatar, id, day);
assertEq(balanceOnDay, 0);
assertEq(discountCost, 0);

// store default balance for avatar
discountedBalances.updateBalance(avatar, id, defaultValue, day);

// returned balance should remain the same on the same day as last update, with a discount cost of 0.
uint64 requestedDay = day;
(balanceOnDay, discountCost) = discountedBalances.balanceOfOnDay(avatar, id, requestedDay);
assertEq(balanceOnDay, defaultValue);
assertEq(discountCost, 0);

// returned balance should be discounted
requestedDay += 31;
(balanceOnDay, discountCost) = discountedBalances.balanceOfOnDay(avatar, id, requestedDay);
assertEq(balanceOnDay + discountCost, defaultValue);
assertTrue(discountCost > 0);
}

// Internal helpers

/// @dev should emit IERC1155.TransferSingle and DiscountCost events (discountCost value check is skipped)
function _expectEmitDiscountEvents(uint256 id, address avatar) internal {
vm.expectEmit(true, true, true, false);
emit IERC1155.TransferSingle(address(this), avatar, address(0), id, 0);
vm.expectEmit(true, true, false, false);
emit IDiscountedBalances.DiscountCost(avatar, id, 0);
}
}
48 changes: 48 additions & 0 deletions test/circles/MockDiscountedBalances.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.13;

import {DiscountedBalances} from "src/circles/DiscountedBalances.sol";

interface IDiscountedBalances {
event DiscountCost(address indexed account, uint256 indexed id, uint256 discountCost);
}

contract MockDiscountedBalances is DiscountedBalances {
// Constructor

constructor(uint256 _inflationDayZero) DiscountedBalances(_inflationDayZero) {}

// External functions

function maxBalance() external pure returns (uint256) {
return MAX_VALUE;
}

function getAvatarBalanceValue(uint256 id, address avatar) external view returns (uint192) {
return discountedBalances[id][avatar].balance;
}

function getAvatarLastUpdatedDayValue(uint256 id, address avatar) external view returns (uint64) {
return discountedBalances[id][avatar].lastUpdatedDay;
}

function getTotalSupplyBalanceValue(uint256 id) external view returns (uint192) {
return discountedTotalSupplies[id].balance;
}

function getTotalSupplyLastUpdatedDayValue(uint256 id) external view returns (uint64) {
return discountedTotalSupplies[id].lastUpdatedDay;
}

function updateBalance(address avatar, uint256 id, uint256 newBalance, uint64 newDay) external {
_updateBalance(avatar, id, newBalance, newDay);
}

function discountAndAddToBalance(address avatar, uint256 id, uint256 addedValue, uint64 newDay) external {
_discountAndAddToBalance(avatar, id, addedValue, newDay);
}

function updateTotalSupply(uint256 id, uint192 _balance, uint64 _day) external {
discountedTotalSupplies[id] = DiscountedBalance({balance: _balance, lastUpdatedDay: _day});
}
}

0 comments on commit 653f1ed

Please sign in to comment.