Skip to content

Commit

Permalink
Add events to Redemption Controller (#102)
Browse files Browse the repository at this point in the history
- Add event `RedemptionCreated` for each time a redemption is created
(after an offer)
- Add event `RedemptionUpdate` for each time the user redeems

close #101
  • Loading branch information
sirnicolaz authored Oct 27, 2023
1 parent 9b6a96d commit 4d78acf
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 9 deletions.
55 changes: 47 additions & 8 deletions contracts/RedemptionController/RedemptionControllerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.16;

import "./IRedemptionController.sol";
import "hardhat/console.sol";

// The contract tells how many tokens are redeemable by Contributors
abstract contract RedemptionControllerBase is IRedemptionController {
Expand Down Expand Up @@ -29,6 +30,21 @@ abstract contract RedemptionControllerBase is IRedemptionController {
mapping(address => MintBudget[]) internal _mintBudgets;
mapping(address => uint256) internal _mintBudgetsStartIndex;

event RedemptionCreated(
address account,
uint256 index,
uint256 amount,
uint256 starts,
uint256 ends
);

event RedemptionUpdated(
address from,
uint256 index,
uint256 amountRequested,
uint256 amountRedeemed
);

function _initialize() internal {
redemptionStart = 60 days;
redemptionWindow = 10 days;
Expand Down Expand Up @@ -69,7 +85,7 @@ abstract contract RedemptionControllerBase is IRedemptionController {
}

function _afterOffer(address account, uint256 amount) internal virtual {
// Find tokens minted ofer the last 3 months of activity, no earlier than 15 months
// Find tokens minted over the last 3 months of activity, no earlier than 15 months
if (_mintBudgets[account].length == 0) {
return;
}
Expand Down Expand Up @@ -192,28 +208,44 @@ abstract contract RedemptionControllerBase is IRedemptionController {
}
}

function _afterRedeem(address account, uint256 amount) internal virtual {
function _afterRedeem(
address account,
uint256 amountRequested
) internal virtual {
Redeemable[] storage redeemables = _redeemables[account];
uint256 amountLeft = amountRequested;

for (uint256 i = 0; i < redeemables.length && amount > 0; i++) {
for (uint256 i = 0; i < redeemables.length && amountLeft > 0; i++) {
Redeemable storage redeemable = redeemables[i];
if (
block.timestamp >= redeemable.start &&
block.timestamp < redeemable.end
) {
if (amount < redeemable.amount) {
redeemable.amount -= amount;
amount = 0;
if (amountLeft < redeemable.amount) {
redeemable.amount -= amountLeft;
emit RedemptionUpdated(
account,
i,
amountRequested,
amountLeft
);
amountLeft = 0;
} else {
amount -= redeemable.amount;
amountLeft -= redeemable.amount;
emit RedemptionUpdated(
account,
i,
amountRequested,
redeemable.amount
);
redeemable.amount = 0;
// FIXME: delete object from array?
}
}
}

require(
amount == 0,
amountLeft == 0,
"Redemption controller: amount exceeds redeemable balance"
);
}
Expand All @@ -232,6 +264,13 @@ abstract contract RedemptionControllerBase is IRedemptionController {
redemptionStarts,
redemptionStarts + redemptionWindow
);
emit RedemptionCreated(
account,
_redeemables[account].length,
amount,
offerRedeemable.start,
offerRedeemable.end
);
_redeemables[account].push(offerRedeemable);
}
}
126 changes: 125 additions & 1 deletion test/RedemptionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ import {
RedemptionController__factory,
} from "../typechain";

import { mineEVMBlock, timeTravel } from "./utils/evm";
import {
getEVMTimestamp,
mineEVMBlock,
setEVMTimestamp,
timeTravel,
} from "./utils/evm";
import { roles } from "./utils/roles";

chai.use(solidity);
chai.use(chaiAsPromised);
const { expect } = chai;

const DAY = 3600 * 24;

describe("RedemptionController", () => {
let snapshotId: string;

Expand Down Expand Up @@ -92,6 +99,90 @@ describe("RedemptionController", () => {
`AccessControl: account ${account.address.toLowerCase()} is missing role ${TOKEN_MANAGER_ROLE}`
);
});

describe("when 10 tokens are minted", async () => {
async function _timestamps() {
const nextTimestamp = (await getEVMTimestamp()) + 1;
await setEVMTimestamp(nextTimestamp);
const expectedStart = nextTimestamp + 60 * DAY;
const expectedEnd = nextTimestamp + 70 * DAY;

return [expectedStart, expectedEnd];
}

beforeEach(async () => {
await redemptionController.afterMint(account.address, 10);
});

it("emits a RedeemCreated with 10 tokens and expiration data after offer", async () => {
const [expectedStart, expectedEnd] = await _timestamps();
await expect(redemptionController.afterOffer(account.address, 10))
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 0, 10, expectedStart, expectedEnd);
});

it("emits a RedeemCreated with partially matched mints and expiration data after offer", async () => {
await redemptionController.afterOffer(account.address, 7);
const [expectedStart, expectedEnd] = await _timestamps();
await expect(redemptionController.afterOffer(account.address, 10))
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 1, 3, expectedStart, expectedEnd);
});

it("emits a RedeemCreated from multiple mints with incremental ids", async () => {
await redemptionController.afterMint(account.address, 12);
const [expectedStart, expectedEnd] = await _timestamps();
await expect(redemptionController.afterOffer(account.address, 22))
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 0, 10, expectedStart, expectedEnd)
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 1, 12, expectedStart, expectedEnd);
});

it("emits a RedeemCreated from expired redemptions", async () => {
await redemptionController.afterOffer(account.address, 10);
await timeTravel(70, true); // redemption expires
const [expectedStart, expectedEnd] = await _timestamps();
await expect(redemptionController.afterOffer(account.address, 10))
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 1, 10, expectedStart, expectedEnd);
});

it("emits a RedeemCreated from partially matched expired redemptions", async () => {
await redemptionController.afterOffer(account.address, 10);
await timeTravel(60, true); // redemption starts
await redemptionController.afterRedeem(account.address, 7);
await timeTravel(10, true); // redemption starts
const [expectedStart, expectedEnd] = await _timestamps();
await expect(redemptionController.afterOffer(account.address, 10))
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 1, 3, expectedStart, expectedEnd);
});

it("emits a RedeemCreated from multiple expired redemptions, partially", async () => {
await redemptionController.afterOffer(account.address, 6);
await redemptionController.afterOffer(account.address, 4);
await timeTravel(70, true); // redemption expires
const [expectedStart, expectedEnd] = await _timestamps();
await expect(redemptionController.afterOffer(account.address, 9))
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 2, 6, expectedStart, expectedEnd)
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 3, 3, expectedStart, expectedEnd);
});

it("emits a RedeemCreated from multiple expired redemptions, completely", async () => {
await redemptionController.afterOffer(account.address, 6);
await redemptionController.afterOffer(account.address, 4);
await timeTravel(70, true); // redemption expires
const [expectedStart, expectedEnd] = await _timestamps();
await expect(redemptionController.afterOffer(account.address, 15))
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 2, 6, expectedStart, expectedEnd)
.to.emit(redemptionController, "RedemptionCreated")
.withArgs(account.address, 3, 4, expectedStart, expectedEnd);
});
});
});

describe("afterRedeem", async () => {
Expand All @@ -110,6 +201,39 @@ describe("RedemptionController", () => {
"Redemption controller: amount exceeds redeemable balance"
);
});

describe.only("when 10 tokens are redeemable", async () => {
beforeEach(async () => {
await redemptionController.afterMint(account.address, 10);
await redemptionController.afterOffer(account.address, 10);
await timeTravel(60, true);
});

it("emits a RedeemUpdated upon full redemption", async () => {
await expect(redemptionController.afterRedeem(account.address, 10))
.to.emit(redemptionController, "RedemptionUpdated")
.withArgs(account.address, 0, 10, 10);
});

it("emits a RedeemUpdated upon partial redemption", async () => {
await expect(redemptionController.afterRedeem(account.address, 7))
.to.emit(redemptionController, "RedemptionUpdated")
.withArgs(account.address, 0, 7, 7);
});

it("emits a RedeemUpdated upon partial redemption for each redemption covering the amount requested", async () => {
await timeTravel(10, true); // expire old redemption
// make two redemptions
await redemptionController.afterOffer(account.address, 7);
await redemptionController.afterOffer(account.address, 3);
await timeTravel(60, true); // redemption time
await expect(redemptionController.afterRedeem(account.address, 10))
.to.emit(redemptionController, "RedemptionUpdated")
.withArgs(account.address, 1, 10, 7)
.to.emit(redemptionController, "RedemptionUpdated")
.withArgs(account.address, 2, 10, 3);
});
});
});

describe("redeemableBalance", async () => {
Expand Down

0 comments on commit 4d78acf

Please sign in to comment.