Skip to content

Commit

Permalink
rewards: use eip-7201 for storage
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrai committed Feb 6, 2024
1 parent 8a5fee9 commit b396012
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 66 deletions.
180 changes: 127 additions & 53 deletions packages/nouns-contracts/contracts/Rewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,6 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/

uint256 public nextAuctionIdToReward;
uint32 public nextProposalIdToReward;
uint256 public nextProposalRewardFirstAuctionId;
uint256 lastProposalRewardsUpdate;

RewardParams public params;

IERC20 public ethToken;
address public admin;

mapping(uint32 clientId => uint256 balance) public _clientBalances;

struct RewardParams {
uint32 minimumRewardPeriod;
uint8 numProposalsEnoughForReward;
Expand All @@ -93,11 +81,34 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
uint8 minimumAuctionsBetweenUpdates;
}

/// @custom:storage-location erc7201:nouns.rewards
struct RewardsStorage {
uint256 nextAuctionIdToReward;
uint32 nextProposalIdToReward;
uint256 nextProposalRewardFirstAuctionId;
uint256 lastProposalRewardsUpdate;
RewardParams params;
IERC20 ethToken;
address admin;
mapping(uint32 clientId => uint256 balance) _clientBalances;
}

/// @dev This is a ERC-7201 storage location, calculated using:
/// @dev keccak256(abi.encode(uint256(keccak256("nouns.rewards")) - 1)) & ~bytes32(uint256(0xff));
bytes32 public constant RewardsStorageLocation = 0x9a06af3161ac5b0c3de4e6c981ab9d9f60b530386f5eaae00d541393fbecd700;

function _getRewardsStorage() private pure returns (RewardsStorage storage $) {
assembly {
$.slot := RewardsStorageLocation
}
}

/**
* @dev Throws if called by any account other than the owner or admin.
*/
modifier onlyOwnerOrAdmin() {
require(owner() == _msgSender() || admin == _msgSender(), 'Caller must be owner or admin');
RewardsStorage storage $ = _getRewardsStorage();
require(owner() == _msgSender() || $.admin == _msgSender(), 'Caller must be owner or admin');
_;
}

Expand All @@ -120,15 +131,17 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
RewardParams memory rewardParams,
address descriptor
) public initializer {
RewardsStorage storage $ = _getRewardsStorage();

super.initialize(owner, descriptor);
__Pausable_init_unchained();
admin = admin_;
ethToken = IERC20(ethToken_);
nextProposalIdToReward = nextProposalIdToReward_;
nextAuctionIdToReward = nextAuctionIdToReward_;
nextProposalRewardFirstAuctionId = nextProposalRewardFirstAuctionId_;
params = rewardParams;
lastProposalRewardsUpdate = block.timestamp;
$.admin = admin_;
$.ethToken = IERC20(ethToken_);
$.nextProposalIdToReward = nextProposalIdToReward_;
$.nextAuctionIdToReward = nextAuctionIdToReward_;
$.nextProposalRewardFirstAuctionId = nextProposalRewardFirstAuctionId_;
$.params = rewardParams;
$.lastProposalRewardsUpdate = block.timestamp;
}

/**
Expand All @@ -145,24 +158,27 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
string calldata name,
string calldata description
) public override whenNotPaused returns (uint32) {
RewardsStorage storage $ = _getRewardsStorage();

uint32 tokenId = super.registerClient(name, description);

// Increase the balance by one wei so that the slot is non zero when increased in the future
_clientBalances[tokenId] += 1;
$._clientBalances[tokenId] += 1;

return tokenId;
}

function updateRewardsForAuctions(uint256 lastNounId) public whenNotPaused {
uint256 startGas = gasleft();
RewardsStorage storage $ = _getRewardsStorage();

bool sawNonZeroClientId = false;
uint256 nextAuctionIdToReward_ = nextAuctionIdToReward;
uint256 nextAuctionIdToReward_ = $.nextAuctionIdToReward;
require(
lastNounId >= nextAuctionIdToReward_ + params.minimumAuctionsBetweenUpdates,
lastNounId >= nextAuctionIdToReward_ + $.params.minimumAuctionsBetweenUpdates,
'lastNounId must be higher'
);
nextAuctionIdToReward = lastNounId + 1;
$.nextAuctionIdToReward = lastNounId + 1;

INounsAuctionHouseV2.Settlement[] memory settlements = auctionHouse.getSettlements(
nextAuctionIdToReward_,
Expand All @@ -182,11 +198,11 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
}
}

uint16 auctionRewardBps = params.auctionRewardBps;
uint16 auctionRewardBps = $.params.auctionRewardBps;
for (uint256 i = 1; i < m.nextAvailableIndex; ++i) {
uint256 reward = (m.values[i].balance * auctionRewardBps) / 10_000;
uint32 clientId = m.values[i].clientId;
_clientBalances[clientId] += reward;
$._clientBalances[clientId] += reward;

emit ClientRewarded(clientId, reward);
}
Expand All @@ -207,6 +223,7 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
uint256 proposalRewardForPeriod;
uint256 votingRewardForPeriod;
uint32 nextProposalIdToReward;
NounsDAOV3Types.ProposalForRewards lastProposal;
}

/**
Expand All @@ -220,10 +237,11 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
uint32[] calldata votingClientIds
) public whenNotPaused {
uint256 startGas = gasleft();
RewardsStorage storage $ = _getRewardsStorage();

Temp memory t;

t.nextProposalIdToReward = nextProposalIdToReward;
t.nextProposalIdToReward = $.nextProposalIdToReward;

require(lastProposalId <= nounsDAO.proposalCount(), 'bad lastProposalId');
require(lastProposalId >= t.nextProposalIdToReward, 'bad lastProposalId');
Expand All @@ -236,22 +254,22 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
lastProposalId,
votingClientIds
);
nextProposalIdToReward = lastProposalId + 1;
$.nextProposalIdToReward = lastProposalId + 1;

NounsDAOV3Types.ProposalForRewards memory lastProposal = proposals[proposals.length - 1];
t.lastProposal = proposals[proposals.length - 1];

(uint256 auctionRevenue, uint256 lastAuctionId) = getAuctionRevenue({
firstNounId: nextProposalRewardFirstAuctionId,
endTimestamp: lastProposal.creationTimestamp
firstNounId: $.nextProposalRewardFirstAuctionId,
endTimestamp: t.lastProposal.creationTimestamp
});
nextProposalRewardFirstAuctionId = lastAuctionId + 1;
$.nextProposalRewardFirstAuctionId = lastAuctionId + 1;

require(auctionRevenue > 0, 'auctionRevenue must be > 0');

t.proposalRewardForPeriod = (auctionRevenue * params.proposalRewardBps) / 10_000;
t.votingRewardForPeriod = (auctionRevenue * params.votingRewardBps) / 10_000;
t.proposalRewardForPeriod = (auctionRevenue * $.params.proposalRewardBps) / 10_000;
t.votingRewardForPeriod = (auctionRevenue * $.params.votingRewardBps) / 10_000;

uint16 proposalEligibilityQuorumBps_ = params.proposalEligibilityQuorumBps;
uint16 proposalEligibilityQuorumBps_ = $.params.proposalEligibilityQuorumBps;

//// First loop over the proposals:
//// 1. Make sure all proposals have finished voting.
Expand Down Expand Up @@ -284,13 +302,13 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
//// 2.b. At least `minimumRewardPeriod` seconds have passed since the last update.

require(t.numEligibleProposals > 0, 'at least one eligible proposal');
if (t.numEligibleProposals < params.numProposalsEnoughForReward) {
if (t.numEligibleProposals < $.params.numProposalsEnoughForReward) {
require(
lastProposal.creationTimestamp > lastProposalRewardsUpdate + params.minimumRewardPeriod,
t.lastProposal.creationTimestamp > $.lastProposalRewardsUpdate + $.params.minimumRewardPeriod,
'not enough time passed'
);
}
lastProposalRewardsUpdate = lastProposal.creationTimestamp;
$.lastProposalRewardsUpdate = t.lastProposal.creationTimestamp;

// Calculate the reward per proposal and per vote
t.rewardPerProposal = t.proposalRewardForPeriod / t.numEligibleProposals;
Expand Down Expand Up @@ -334,7 +352,7 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
for (uint256 i = 1; i < m.nextAvailableIndex; ++i) {
uint256 reward = m.values[i].balance;
uint32 clientId = m.values[i].clientId;
_clientBalances[clientId] += reward;
$._clientBalances[clientId] += reward;

emit ClientRewarded(clientId, reward);
}
Expand All @@ -352,14 +370,16 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
* @param to the address to withdraw to
*/
function withdrawClientBalance(uint32 clientId, uint256 amount, address to) public whenNotPaused {
RewardsStorage storage $ = _getRewardsStorage();

require(ownerOf(clientId) == msg.sender, 'must be client NFT owner');
require(amount < _clientBalances[clientId], 'amount too large');
require(amount < $._clientBalances[clientId], 'amount too large');

_clientBalances[clientId] -= amount;
$._clientBalances[clientId] -= amount;

emit ClientBalanceWithdrawal(clientId, amount, to);

ethToken.safeTransfer(to, amount);
$.ethToken.safeTransfer(to, amount);
}

/**
Expand All @@ -373,7 +393,9 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
* @dev accounts for the extra wei used for gas optimization
*/
function clientBalance(uint32 clientId) public view returns (uint256) {
uint256 balance = _clientBalances[clientId];
RewardsStorage storage $ = _getRewardsStorage();

uint256 balance = $._clientBalances[clientId];
if (balance > 0) {
// accounting for the extra 1 wei added to the balance for gas optimizations
balance--;
Expand All @@ -386,13 +408,15 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
* @dev This is not meant to be called onchain because it may be very gas intensive.
*/
function getVotingClientIds(uint32 lastProposalId) public view returns (uint32[] memory) {
RewardsStorage storage $ = _getRewardsStorage();

uint256 numClientIds = nextTokenId();
uint32[] memory allClientIds = new uint32[](numClientIds);
for (uint32 i; i < numClientIds; ++i) {
allClientIds[i] = i;
}
NounsDAOV3Types.ProposalForRewards[] memory proposals = nounsDAO.proposalDataForRewards(
nextProposalIdToReward,
$.nextProposalIdToReward,
lastProposalId,
allClientIds
);
Expand All @@ -417,10 +441,6 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
return nonZeroClientIds;
}

function getParams() public view returns (RewardParams memory) {
return params;
}

function getAuctionRevenue(
uint256 firstNounId,
uint256 endTimestamp
Expand All @@ -434,14 +454,64 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
lastAuctionId = s[s.length - 1].nounId;
}

/**
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
* PUBLIC READ - STORAGE GETTERS
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/

function nextAuctionIdToReward() public view returns (uint256) {
RewardsStorage storage $ = _getRewardsStorage();
return $.nextAuctionIdToReward;
}

function nextProposalIdToReward() public view returns (uint32) {
RewardsStorage storage $ = _getRewardsStorage();
return $.nextProposalIdToReward;
}

function nextProposalRewardFirstAuctionId() public view returns (uint256) {
RewardsStorage storage $ = _getRewardsStorage();
return $.nextProposalRewardFirstAuctionId;
}

function lastProposalRewardsUpdate() public view returns (uint256) {
RewardsStorage storage $ = _getRewardsStorage();
return $.lastProposalRewardsUpdate;
}

function getParams() public view returns (RewardParams memory) {
RewardsStorage storage $ = _getRewardsStorage();
return $.params;
}

function ethToken() public view returns (IERC20) {
RewardsStorage storage $ = _getRewardsStorage();
return $.ethToken;
}

function admin() public view returns (address) {
RewardsStorage storage $ = _getRewardsStorage();
return $.admin;
}

/**
* @notice Returns the raw value from _clientBalances mapping. Usually you want to use `clientBalance`
*/
function _clientBalances(uint32 clientId) public view returns (uint256) {
RewardsStorage storage $ = _getRewardsStorage();
return $._clientBalances[clientId];
}

/**
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
* ADMIN
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/

function setParams(RewardParams calldata newParams) public onlyOwner {
params = newParams;
RewardsStorage storage $ = _getRewardsStorage();
$.params = newParams;
}

function pause() public onlyOwnerOrAdmin {
Expand All @@ -453,11 +523,13 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {
}

function setAdmin(address newAdmin) public onlyOwner {
admin = newAdmin;
RewardsStorage storage $ = _getRewardsStorage();
$.admin = newAdmin;
}

function setETHToken(address newToken) public onlyOwner {
ethToken = IERC20(newToken);
RewardsStorage storage $ = _getRewardsStorage();
$.ethToken = IERC20(newToken);
}

function withdrawToken(address token, address to, uint256 amount) public onlyOwner {
Expand All @@ -478,16 +550,18 @@ contract Rewards is NounsClientToken, UUPSUpgradeable, PausableUpgradeable {

/// @dev refunds gas using the `ethToken` instead of ETH
function _refundGas(uint256 startGas) internal {
RewardsStorage storage $ = _getRewardsStorage();
IERC20 ethToken_ = $.ethToken;
unchecked {
uint256 balance = ethToken.balanceOf(address(this));
uint256 balance = ethToken_.balanceOf(address(this));
if (balance == 0) {
return;
}
uint256 basefee = min(block.basefee, MAX_REFUND_BASE_FEE);
uint256 gasPrice = min(tx.gasprice, basefee + MAX_REFUND_PRIORITY_FEE);
uint256 gasUsed = startGas - gasleft() + REFUND_BASE_GAS;
uint256 refundAmount = min(gasPrice * gasUsed, balance);
ethToken.safeTransfer(tx.origin, refundAmount);
ethToken_.safeTransfer(tx.origin, refundAmount);
}
}

Expand Down
Loading

0 comments on commit b396012

Please sign in to comment.