Skip to content

Commit

Permalink
feat(Grants): paginate storage and remove max grant limit (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
aliXsed authored Sep 3, 2024
1 parent 460c9e6 commit a76f669
Show file tree
Hide file tree
Showing 3 changed files with 375 additions and 100 deletions.
173 changes: 111 additions & 62 deletions src/Grants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
contract Grants {
using SafeERC20 for IERC20;

// Maximum number of vesting schedules per address. This is a safety bound to limit the max gas cost of operations.
uint32 public constant MAX_SCHEDULES = 100;

// Token used for vesting.
IERC20 public immutable token;

// Minimum amount of tokens that can be vested per period. This is a safety bound to prevent dusting attacks.
uint256 public immutable perPeriodMinAmount;

// Mapping from recipient address to array of vesting schedules.
mapping(address => VestingSchedule[]) public vestingSchedules;
// Maximum number of vesting schedules per address per page.
uint8 public immutable pageLimit;

// Mapping of addresses to the current page of their vesting schedules.
// The current page is the last page that has been used to store a vesting schedule.
mapping(address => uint256) public currentPage;

// Mapping of addresses to their vesting schedules split into pages.
mapping(address => mapping(uint256 => VestingSchedule[])) public vestingSchedules;

struct VestingSchedule {
address cancelAuthority; // Address authorized to cancel the vesting.
Expand All @@ -35,20 +39,28 @@ contract Grants {

// Events
event VestingScheduleAdded(address indexed to, VestingSchedule schedule);
event Claimed(address indexed who, uint256 amount);
event VestingSchedulesCanceled(address indexed from, address indexed to);
event Renounced(address indexed from, address indexed to);
// start and end indicate the range of the grant pages that are iterated over for claiming.
event Claimed(address indexed who, uint256 amount, uint256 start, uint256 end);
// start and end indicate the range of the grant pages that are iterated over for cancelling.
event VestingSchedulesCanceled(address indexed from, address indexed to, uint256 start, uint256 end);
// start and end indicate the range of the grant pages that are iterated over for renouncing.
event Renounced(address indexed from, address indexed to, uint256 start, uint256 end);

// Errors
error InvalidZeroParameter(); // Parameters such as some addresses and periods, periodCounts must be non-zero.
error VestingToSelf(); // Thrown when the creator attempts to vest tokens to themselves.
error MaxSchedulesReached(); // Thrown when the addition of a new schedule would exceed the maximum allowed.
error NoOpIsFailure(); // Thrown when an operation that should change state does not.
error LowVestingAmount(); // Thrown when the amount to be vested is below the minimum allowed.
error InvalidPage(); // Thrown when the page is invalid.

constructor(address _token, uint256 _perPeriodMinAmount, uint8 _pageLimit) {
if (_pageLimit == 0) {
revert InvalidZeroParameter();
}

constructor(address _token, uint256 _perPeriodMinAmount) {
token = IERC20(_token);
perPeriodMinAmount = _perPeriodMinAmount;
pageLimit = _pageLimit;
}

/**
Expand All @@ -72,47 +84,60 @@ contract Grants {
_mustNotBeSelf(to);
_mustBeNonZero(period);
_mustBeNonZero(periodCount);
_mustNotExceedMaxSchedules(to);
_mustBeEqualOrExceedMinAmount(perPeriodAmount);

token.safeTransferFrom(msg.sender, address(this), perPeriodAmount * periodCount);

VestingSchedule memory schedule = VestingSchedule(cancelAuthority, start, period, periodCount, perPeriodAmount);
vestingSchedules[to].push(schedule);

uint256 page = currentPage[to];
if (vestingSchedules[to][page].length == pageLimit) {
page += 1;
currentPage[to] = page;
}
vestingSchedules[to][page].push(schedule);

emit VestingScheduleAdded(to, schedule);
}

/**
* @notice Claims all vested tokens available for msg.sender up to the current block timestamp.
* @param start Start page of the vesting schedules to claim from.
* @param end the page after the last page of the vesting schedules to claim from.
* @dev if start == end == 0, all pages will be used for claim.
*/
function claim() external {
function claim(uint256 start, uint256 end) external {
uint256 totalClaimable = 0;
uint256 currentTime = block.timestamp;

uint256 i = 0;
VestingSchedule[] storage schedules = vestingSchedules[msg.sender];
while (i < schedules.length) {
VestingSchedule storage schedule = schedules[i];
if (currentTime > schedule.start) {
uint256 periodsElapsed = (currentTime - schedule.start) / schedule.period;
uint256 effectivePeriods = periodsElapsed > schedule.periodCount ? schedule.periodCount : periodsElapsed;
uint256 claimable = effectivePeriods * schedule.perPeriodAmount;
schedule.periodCount -= uint32(effectivePeriods);
schedule.start += periodsElapsed * schedule.period;
totalClaimable += claimable;
if (schedule.periodCount == 0) {
schedules[i] = schedules[schedules.length - 1];
schedules.pop();
continue;
(start, end) = _sanitizePageRange(msg.sender, start, end);

for (uint256 page = start; page < end; page++) {
uint256 i = 0;
VestingSchedule[] storage schedules = vestingSchedules[msg.sender][page];
while (i < schedules.length) {
VestingSchedule storage schedule = schedules[i];
if (currentTime > schedule.start) {
uint256 periodsElapsed = (currentTime - schedule.start) / schedule.period;
uint256 effectivePeriods =
periodsElapsed > schedule.periodCount ? schedule.periodCount : periodsElapsed;
uint256 claimable = effectivePeriods * schedule.perPeriodAmount;
schedule.periodCount -= uint32(effectivePeriods);
schedule.start += periodsElapsed * schedule.period;
totalClaimable += claimable;
if (schedule.periodCount == 0) {
schedules[i] = schedules[schedules.length - 1];
schedules.pop();
continue;
}
}
i++;
}
i++;
}

if (totalClaimable > 0) {
token.safeTransfer(msg.sender, totalClaimable);
emit Claimed(msg.sender, totalClaimable);
emit Claimed(msg.sender, totalClaimable, start, end);
} else {
revert NoOpIsFailure();
}
Expand All @@ -121,49 +146,64 @@ contract Grants {
/**
* @notice Renounces the cancel authority for all of the msg.sender's vesting schedules directed to a specific recipient.
* @param to Recipient of the vesting whose schedules are affected.
* @param start Start page of the vesting schedules to renounce from.
* @param end the page after the last page of the vesting schedules to renounce from.
* @dev if start == end == 0, all pages will be used for renounce.
*/
function renounce(address to) external {
function renounce(address to, uint256 start, uint256 end) external {
bool anySchedulesFound = false;
VestingSchedule[] storage schedules = vestingSchedules[to];
for (uint256 i = 0; i < schedules.length; i++) {
if (schedules[i].cancelAuthority == msg.sender) {
schedules[i].cancelAuthority = address(0);
anySchedulesFound = true;

(start, end) = _sanitizePageRange(to, start, end);
for (uint256 page = start; page < end; page++) {
VestingSchedule[] storage schedules = vestingSchedules[to][page];
for (uint256 i = 0; i < schedules.length; i++) {
if (schedules[i].cancelAuthority == msg.sender) {
schedules[i].cancelAuthority = address(0);
anySchedulesFound = true;
}
}
}

if (!anySchedulesFound) {
revert NoOpIsFailure();
} else {
emit Renounced(msg.sender, to);
emit Renounced(msg.sender, to, start, end);
}
}

/**
* @notice Cancels all vesting schedules of a specific recipient, initiated by the cancel authority.
* @param to Recipient whose schedules will be canceled.
* @param start Start page of the vesting schedules to cancel.
* @param end the page after the last page of the vesting schedules to cancel.
* @dev if start == end == 0, all pages will be used for cancel.
*/
function cancelVestingSchedules(address to) external {
function cancelVestingSchedules(address to, uint256 start, uint256 end) external {
uint256 totalClaimable = 0;
uint256 totalRedeemable = 0;
uint256 currentTime = block.timestamp;

uint256 i = 0;
VestingSchedule[] storage schedules = vestingSchedules[to];
while (i < schedules.length) {
VestingSchedule storage schedule = schedules[i];
if (schedule.cancelAuthority == msg.sender) {
uint256 periodsElapsed =
currentTime > schedule.start ? (currentTime - schedule.start) / schedule.period : 0;
uint256 effectivePeriods = periodsElapsed > schedule.periodCount ? schedule.periodCount : periodsElapsed;
uint256 claimable = effectivePeriods * schedule.perPeriodAmount;
uint256 redeemable = (schedule.periodCount - effectivePeriods) * schedule.perPeriodAmount;
totalClaimable += claimable;
totalRedeemable += redeemable;
schedules[i] = schedules[schedules.length - 1];
schedules.pop();
continue;
(start, end) = _sanitizePageRange(to, start, end);
for (uint256 page = start; page < end; page++) {
uint256 i = 0;
VestingSchedule[] storage schedules = vestingSchedules[to][page];
while (i < schedules.length) {
VestingSchedule storage schedule = schedules[i];
if (schedule.cancelAuthority == msg.sender) {
uint256 periodsElapsed =
currentTime > schedule.start ? (currentTime - schedule.start) / schedule.period : 0;
uint256 effectivePeriods =
periodsElapsed > schedule.periodCount ? schedule.periodCount : periodsElapsed;
uint256 claimable = effectivePeriods * schedule.perPeriodAmount;
uint256 redeemable = (schedule.periodCount - effectivePeriods) * schedule.perPeriodAmount;
totalClaimable += claimable;
totalRedeemable += redeemable;
schedules[i] = schedules[schedules.length - 1];
schedules.pop();
continue;
}
i++;
}
i++;
}

if (totalClaimable == 0 && totalRedeemable == 0) {
Expand All @@ -178,7 +218,7 @@ contract Grants {
token.safeTransfer(msg.sender, totalRedeemable);
}

emit VestingSchedulesCanceled(msg.sender, to);
emit VestingSchedulesCanceled(msg.sender, to, start, end);
}

/**
Expand All @@ -187,7 +227,11 @@ contract Grants {
* @return The number of vesting schedules associated with the address.
*/
function getGrantsCount(address to) external view returns (uint256) {
return vestingSchedules[to].length;
uint256 count = 0;
for (uint256 i = 0; i <= currentPage[to]; i++) {
count += vestingSchedules[to][i].length;
}
return count;
}

// Private helper functions
Expand All @@ -210,15 +254,20 @@ contract Grants {
}
}

function _mustNotExceedMaxSchedules(address to) private view {
if (vestingSchedules[to].length >= MAX_SCHEDULES) {
revert MaxSchedulesReached();
}
}

function _mustBeEqualOrExceedMinAmount(uint256 amount) private view {
if (amount < perPeriodMinAmount) {
revert LowVestingAmount();
}
}

function _sanitizePageRange(address grantee, uint256 start, uint256 end) private view returns (uint256, uint256) {
uint256 endPage = currentPage[grantee] + 1;
if (start > end || start >= endPage) {
revert InvalidPage();
}
if (end > endPage || end == 0) {
end = endPage;
}
return (start, end);
}
}
Loading

0 comments on commit a76f669

Please sign in to comment.