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

Fix: Double-counting time after hat reactivation in HatsTimeFrameModule #124

Merged
merged 6 commits into from
Oct 19, 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
57 changes: 50 additions & 7 deletions pkgs/contract/contracts/timeframe/HatsTimeFrameModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,18 @@ contract HatsTimeFrameModule is
HatsModule,
IHatsTimeFrameModule
{
// hatId => wearer => wore timestamp
mapping(uint256 => mapping(address => uint256)) private woreTime;

// hatId => wearer => last deactivation timestamp
mapping(uint256 => mapping(address => uint256)) private deactivatedTime;

// hatId => wearer => total active time
mapping(uint256 => mapping(address => uint256)) private totalActiveTime;

// hatId => wearer => isActive
mapping(uint256 => mapping(address => bool)) private isActive;

/**
* @dev Constructor to initialize the trusted forwarder.
* @param _trustedForwarder Address of the trusted forwarder contract.
Expand All @@ -22,8 +32,37 @@ contract HatsTimeFrameModule is
) ERC2771Context(_trustedForwarder) HatsModule(_version) {}

function mintHat(uint256 hatId, address wearer) external {
HATS().mintHat(hatId, wearer);
_setWoreTime(wearer, hatId);
isActive[hatId][wearer] = true;
HATS().mintHat(hatId, wearer);
}

/**
* @dev Deactivate the hat, pausing the contribution time.
* Calculate the contribution time up to deactivation.
* @param wearer The address of the person who received the hat.
* @param hatId The ID of the hat that was minted.
*/
function deactivate(uint256 hatId, address wearer) external {
// msg.sender should be the owner of the hat or parent hat owner
require(isActive[hatId][wearer], "Hat is already inactive");
isActive[hatId][wearer] = false;
deactivatedTime[hatId][wearer] = block.timestamp;
totalActiveTime[hatId][wearer] +=
block.timestamp -
woreTime[hatId][wearer];
}

/**
* @dev Reactivate the hat, resuming the contribution time.
* Reset woreTime for new active period.
* @param wearer The address of the person who received the hat.
* @param hatId The ID of the hat that was minted.
*/
function reactivate(uint256 hatId, address wearer) external {
require(!isActive[hatId][wearer], "Hat is already active");
isActive[hatId][wearer] = true;
woreTime[hatId][wearer] = block.timestamp;
}

/**
Expand All @@ -50,6 +89,8 @@ contract HatsTimeFrameModule is

/**
* @dev Gets the elapsed time in seconds since the specific hat was minted for a specific address.
* If the hat is active, calculate time from the last wear time to the current time.
* If the hat is inactive, calculate time up to the deactivation.
* @param wearer The address of the person who received the hat.
* @param hatId The ID of the hat that was minted.
* @return The elapsed time in seconds.
Expand All @@ -58,12 +99,14 @@ contract HatsTimeFrameModule is
address wearer,
uint256 hatId
) external view returns (uint256) {
uint256 mintTime = woreTime[hatId][wearer];
require(
mintTime != 0,
"Hat has not been minted for this wearer and hatId"
);
return block.timestamp - mintTime;
uint256 activeTime = totalActiveTime[hatId][wearer];

if (isActive[hatId][wearer]) {
// If active, calculate time from the last woreTime to the current time
activeTime += block.timestamp - woreTime[hatId][wearer];
}

return activeTime;
}

/**
Expand Down
95 changes: 78 additions & 17 deletions pkgs/contract/test/HatsTimeFrameModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,29 +134,90 @@ describe("HatsTimeFrameModule", () => {
}
}

const initialTime = BigInt(await time.latest());

await HatsTimeFrameModule.write.mintHat([
roleHatId,
address1.account?.address!,
]);

const afterMintTime = BigInt(await time.latest());

let woreTime = await HatsTimeFrameModule.read.getWoreTime([
address1.account?.address!,
roleHatId,
]);

expect(woreTime).to.equal(afterMintTime);

await time.increaseTo(initialTime + 100n);

const currentTime1 = BigInt(await time.latest());

let expectedElapsedTime = currentTime1 - woreTime;

let elapsedTime = await HatsTimeFrameModule.read.getWearingElapsedTime([
address1.account?.address!,
roleHatId,
]);

expect(elapsedTime).to.equal(expectedElapsedTime);

await time.increaseTo(initialTime + 200n);

const currentTime2 = BigInt(await time.latest());

expectedElapsedTime = currentTime2 - woreTime;

elapsedTime = await HatsTimeFrameModule.read.getWearingElapsedTime([
address1.account?.address!,
roleHatId,
]);

expect(
await Hats.read.balanceOf([address1.account?.address!, roleHatId])
).equal(BigInt(1));
expect(elapsedTime).to.equal(expectedElapsedTime);

expect(
await HatsTimeFrameModule.read.getWoreTime([
address1.account?.address!,
roleHatId,
])
).equal(BigInt(await time.latest()));
await HatsTimeFrameModule.write.deactivate([
roleHatId,
address1.account?.address!,
]);

await time.increase(100);
const afterDeactivateTime = BigInt(await time.latest());

expect(
await HatsTimeFrameModule.read.getWearingElapsedTime([
address1.account?.address!,
roleHatId,
])
).equal(BigInt(100));
const totalActiveTimeAfterDeactivation = afterDeactivateTime - woreTime;

expectedElapsedTime = totalActiveTimeAfterDeactivation;

// Increase time to initialTime + 250 seconds (during inactivity)
await time.increaseTo(initialTime + 250n);

// Elapsed time should remain the same
elapsedTime = await HatsTimeFrameModule.read.getWearingElapsedTime([
address1.account?.address!,
roleHatId,
]);

expect(elapsedTime).to.equal(expectedElapsedTime);

// Reactivate the hat
await HatsTimeFrameModule.write.reactivate([
roleHatId,
address1.account?.address!,
]);

// Get woreTime after reactivation
woreTime = BigInt(await time.latest());

await time.increaseTo(initialTime + 350n);

const currentTime3 = BigInt(await time.latest());

expectedElapsedTime = totalActiveTimeAfterDeactivation + (currentTime3 - woreTime);

elapsedTime = await HatsTimeFrameModule.read.getWearingElapsedTime([
address1.account?.address!,
roleHatId,
]);

expect(elapsedTime).to.equal(expectedElapsedTime);
});
});
});
Loading