What’s new in v5.1 of OpenZeppelin ?
OpenZeppelin recently announced changes in the upcoming version 5.1.
Here are few imperative changes around Governance and ERC20.
Governance Related Changes
GovernorCountingSimple
- The _countVotes
function now returns uint256
.
Previously the _countVotes function looked something like this:
function _countVote(
uint256 proposalId,
address account,
uint8 support,
uint256 weight,
bytes memory // params
) internal virtual override {
// Voting Logic
}
Now it returns a uint value, which is the total votes casted.
function _countVote(
uint256 proposalId,
address account,
uint8 support,
uint256 totalWeight,
bytes memory // params
) internal virtual override returns (uint256) {
// Voting Logic
return totalWeight;
}
If you are using V5.0 and upgrading to 5.1, consider adding a return statement in the overridden function to avoid any compilation errors. As stated by OZ, this change gives more flexibility for fractional or partial voting.
Speaking of fractional voting, a new extension for Governance, GovernorCountingFractional
that has been in discussion for quite a while now is finally being included in 5.1.
This enhances governance by allowing voting power to be distributed not just between “For”, “Against”, and “Abstain” but also between these categories fractionally.
How it Works
- In traditional governance models, voting power was treated as a whole unit (like 1 token = 1 vote). However, this new module allows for fractionalized voting, where a portion of a voter's power could be cast for different options.
- For example, a voter could distribute 60% of their voting power towards "For" and 40% towards "Against" on a proposal. This makes the governance process more flexible, especially when dealing with complex decisions that benefit from more nuanced voting outcomes.
Technical details about GovernorCountingFractional
The way _countVotes
worked before was to just receive the intended support and the voting weight as parameters, and update the relevant voting storage inside the proposal struct. This means if I have 100 votes, and I voted FOR a proposal, all of the 100 voting weights will be added to uint256 forWeight
the variable in that proposal.
This was straightforward.
if (support == uint8(VoteType.Against)) {
proposalVote.againstVotes += totalWeight;
} else if (support == uint8(VoteType.For)) {
proposalVote.forVotes += totalWeight;
} else if (support == uint8(VoteType.Abstain)) {
proposalVote.abstainVotes += totalWeight;
}
For fractional voting, let’s see how exactly it works:
Important parameters before we see the function execution.
- support: It’s a
uint8
value. In simple voting, it was used to determine whether the vote was against/for/abstain, we could pass 0,1, or 2 respectively. Based on this enum👇
enum VoteType {
Against,
For,
Abstain
}
- But in Fractional voting, there’s an additional value,
255
.255
because it is the highestuint8
, and it leaves the other values in the enum. - If you see the
_countVotes
function inGovernorCountingSimple
, you’ll notice that it has an unused bytes parameter, namedparams
. This parameter is now being used for Fractional Voting. It is supposed to be passed with exactly three uint128 values, representing against/for/abstain respectively.
Here is the exact flow of the new _countVotes
function for Fractional Voting:
- Remaining Weight Calculation:
- First, the function checks how many votes the voter has left to cast on the proposal by subtracting the votes they've already used from their total voting power. This remaining weight is essentially the available votes they can still use.
(, uint256 remainingWeight) = totalWeight.trySub(usedVotes(proposalId, account));
if (remainingWeight == 0) {
revert GovernorAlreadyCastVote(account);
}
- Default Values:
- The function initializes three values to count votes for "Against," "For," and "Abstain", all starting at zero. Another variable,
usedWeight
, is also initialized to zero, which will be used to track how many votes the voter actually cast during this operation.
- The function initializes three values to count votes for "Against," "For," and "Abstain", all starting at zero. Another variable,
uint256 againstVotes = 0;
uint256 forVotes = 0;
uint256 abstainVotes = 0;
uint256 usedWeight = 0;
- Check the Voting Type:
The function looks at the support parameter to determine what type of vote is being cast:- If it's 0 (Against), the function assumes the voter is using all their remaining weight to vote against the proposal.
- If it's 1 (For), the function assigns all the remaining weight to voting for the proposal.
- If it's 2 (Abstain), the voter is abstaining, and all their votes are used for that.
- If it's 255 (Fractional), the function knows the voter is splitting their votes across all three categories (Against, For, Abstain).
- An important point to note - If the support parameter is 0,1 or 2 and the variable
params
is not empty then the function reverts.
if (support == uint8(GovernorCountingSimple.VoteType.Against)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = againstVotes = remainingWeight;
} else if (support == uint8(GovernorCountingSimple.VoteType.For)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = forVotes = remainingWeight;
} else if (support == uint8(GovernorCountingSimple.VoteType.Abstain)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = abstainVotes = remainingWeight;
- Fractional Voting Special Case:
- For fractional voting (support = 255), the function expects the
params
argument to contain three packeduint128
values (one for Against, one for For, and one for Abstain). These are extracted using Solidity’s low-level assembly.
- For fractional voting (support = 255), the function expects the
abi.encodePacked(uint128(againstVotes), uint128(forVotes),uint128(abstainVotes))
- With the extracted value, it now assigns the respective values to
againstVotes
forVotes
abstainVotes
, andusedWeight
(usedWeight
is just the sum of all three). - The function then checks that the total of the fractional votes doesn't exceed the voter’s remaining voting power. If it does, an error is thrown, and the transaction fails.
else if (support == VOTE_TYPE_FRACTIONAL) {
if (params.length != 0x30) revert GovernorInvalidVoteParams();
assembly ("memory-safe") {
againstVotes := shr(128, mload(add(params, 0x20)))
forVotes := shr(128, mload(add(params, 0x30)))
abstainVotes := shr(128, mload(add(params, 0x40)))
usedWeight := add(add(againstVotes, forVotes), abstainVotes)
}
if (usedWeight > remainingWeight) {
revert GovernorExceedRemainingWeight(account, usedWeight, remainingWeight);
}
- Update Vote Counts:
- After determining how many votes the voter is using for each category (Against, For, Abstain), the function updates the total votes for that proposal in the contract’s storage.
ProposalVote storage details = _proposalVotes[proposalId];
if (againstVotes > 0) details.againstVotes += againstVotes;
if (forVotes > 0) details.forVotes += forVotes;
if (abstainVotes > 0) details.abstainVotes += abstainVotes;
- Record the Voter's Usage:
- The function also tracks how many votes this specific voter has used on the proposal, ensuring that they cannot exceed their total voting power on future voting actions for the same proposal.
details.usedVotes[account] += usedWeight;
- The function also tracks how many votes this specific voter has used on the proposal, ensuring that they cannot exceed their total voting power on future voting actions for the same proposal.
- Return the Number of Used Votes:
- Finally, the function returns the total number of votes the voter used during this voting operation.
Use Case for Fractional Voting:
- This can be especially useful in decentralized autonomous organizations (DAOs) where members may want to express a weighted preference rather than a binary choice.
- Fractional voting can also help in situations where there are multiple conflicting interests, giving voters the ability to split their support more precisely.
- Most importantly, this will help when the voter is a delegate which automates voting and is being delegated by multiple holders, the delegate may be a contract. Fractional Voting is gonna help them vote based on the interests of all the holders.
- Voting from tokens that are held by a DeFi pool, L2 with tokens held by a bridge, and shielded pool using zero-knowledge proofs.
New extension for ERC20 - ERC20TemporaryApproval
With the inclusion of two opcodes, tstore
, and tload
, we now can use transient storage which lasts for a period of transaction, no matter how many internal calls that transaction performs, unlike memory
which lasts for a single call, and storage
, which is stored in the blockchain state permanently.
Keeping this in mind, a new ERC has been proposed, ERC7674. Which is still in draft. Openzeppelin has already added its implementation, which is also a draft implementation as of now.
Storage Handling
Slot Derivation: The storage slot for temporary allowances is derived using the SlotDerivation
(this is also a newly added library in V5.1) and StorageSlot
libraries. These utilities ensure that the temporary allowances of different users (owner and spender combinations) are stored in distinct slots, derived by applying hash functions.
bytes32 private constant ERC20_TEMPORARY_APPROVAL_STORAGE =
0xea2d0e77a01400d0111492b1321103eed560d8fe44b9a7c2410407714583c400;
The constant ERC20_TEMPORARY_APPROVAL_STORAGE
is used as a base, from which the contract derives mappings for specific owner and spender addresses.
return
ERC20_TEMPORARY_APPROVAL_STORAGE.deriveMapping(owner).deriveMapping(spender).asUint256();
Here, deriveMapping
creates a unique mapping (storage position) for the pair (owner, spender)
.
Allowance Function
Temporary Allowance Addition: When querying the current allowance (allowance
function), the contract sums both persistent allowances (from the regular approve
function) and temporary allowances.
(bool success, uint256 amount) = Math.tryAdd(
super.allowance(owner, spender),
_temporaryAllowance(owner, spender)
);
It checks if the total is within bounds, avoiding overflow. If the sum of both exceeds the maximum uint256
, it returns type(uint256).max
.
Temporary Approval (temporaryApprove
)
- No Event Emission: The function
temporaryApprove
assigns a temporary allowance to aspender
on behalf of themsg.sender
. However, unlikeapprove
, it does not emit anApproval
event.
function temporaryApprove(address spender, uint256 value) public virtual returns (bool) {
_temporaryApprove(_msgSender(), spender, value);
return true;
}
- Internal Storage: The internal function
_temporaryApprove
stores the allowance using the derived slot for(owner, spender)
.
_temporaryAllowanceSlot(owner, spender).tstore(value);
- Here,
tstore
a custom function in theStorageSlot
library that stores data in the derived slot using thetstore
opcode, that will last for the duration of a transaction.
Spending Allowance
- Allowance Spending Logic: The
_spendAllowance
the function consumes the temporary allowance before the regular allowance.
uint256 currentTemporaryAllowance = _temporaryAllowance(owner, spender);
If a temporary allowance is present and sufficient, the contract spends it first. If it’s not sufficient, it reduces the temporary allowance, and the remaining value is deducted from the persistent allowance. This reduces unnecessary state changes and potentially lowers gas costs.
uint256 spendTemporaryAllowance = Math.min(currentTemporaryAllowance, value);
It reduces the temporary allowance via:
_temporaryApprove(owner, spender, currentTemporaryAllowance - spendTemporaryAllowance);
Gas and Storage Efficiency
- The contract appears to optimize for gas costs by using storage slots more efficiently. It may avoid the high costs associated with
sstore
for short-term approvals, possibly leveraging a custom storage or transient storage mechanism for lower gas fees. Although the discussions are still going on as of now.