-
Notifications
You must be signed in to change notification settings - Fork 18
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
Optimize storage of balance checkpoints in T token contract #19
Conversation
cygnusv
commented
Oct 5, 2021
- Uses new implementation for GovernorBravo's Checkpoints borrowed from OpenZeppelin, but with an additional optimization that fits 2 checkpoints per storage slot.
- This produces, on average, a reduction of 20,000 gas per transfer (from 108,000 to 88,000)
- Since we will use checkpoints functionality in the T staking contract and for the covT token, I extracted this functionality to an abstract contract.
- Adds checkpoints to track historical total supply (required for Threshold DAO governance parameters that are defined as percentages of total supply)
* Based on OpenZeppelin's ERC20Votes, that uses an array instead of a mapping to store checkpoints * Instead of storing structs (that always use full slots), use uint128 as the base type. In combination with the array structure, 2 checkpoints fit in 1 storage slot * Maintains the Checkpoint struct as API
* Reduces average transfer cost from 108k gas to 88k. * Removes ~130 LOC from T token contract by using common code from the Checkpoints contract and OpenZeppelin utils
Necessary to test some checkpoint features
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work!
const block = await mineBlock() | ||
expect(await t.getPastTotalSupply(block - 1)).to.equal(initialBalance) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can remove this test and expand context("when minted several times",
at line 822 into:
context("when minted just once", () => {
it("should keep track of historic supply", async () => {
const lastBlock = await mineBlock()
expect(await t.getPastTotalSupply(lastBlock - 1)).to.equal(
initialBalance
)
})
})
context("when minted several times", () => {
context("when minted to the same account", () => {
let block1
let block2
let block3
beforeEach(async () => {
await t.connect(thirdParty).delegate(delegatee.address)
await t.connect(deployer).mint(thirdParty.address, to1e18(10))
block1 = await lastBlockNumber()
await t.connect(deployer).mint(thirdParty.address, to1e18(12))
block2 = await lastBlockNumber()
await t.connect(deployer).mint(thirdParty.address, to1e18(15))
block3 = await lastBlockNumber()
await t.connect(deployer).mint(thirdParty.address, to1e18(17))
})
it("should update current votes", async () => {
expect(await t.getCurrentVotes(thirdParty.address)).to.equal(0)
expect(await t.getCurrentVotes(delegatee.address)).to.equal(
to1e18(54)
)
})
it("should keep track of prior votes", async () => {
expect(await t.getPriorVotes(delegatee.address, block1)).to.equal(
to1e18(10)
)
expect(await t.getPriorVotes(delegatee.address, block2)).to.equal(
to1e18(22)
)
expect(await t.getPriorVotes(delegatee.address, block3)).to.equal(
to1e18(37)
)
})
it("should keep track of historic supply", async () => {
expect(await t.getPastTotalSupply(block1 - 1)).to.equal(
initialBalance
)
expect(await t.getPastTotalSupply(block2 - 1)).to.equal(
initialBalance.add(to1e18(10))
)
expect(await t.getPastTotalSupply(block3 - 1)).to.equal(
initialBalance.add(to1e18(22))
)
})
})
context("when minted to different accounts", () => {
let block1
let block2
let block3
beforeEach(async () => {
await t.connect(tokenHolder).delegate(delegatee.address)
await t.connect(thirdParty).delegate(delegatee.address)
await t.connect(deployer).mint(tokenHolder.address, to1e18(10))
block1 = await lastBlockNumber()
await t.connect(deployer).mint(thirdParty.address, to1e18(11))
block2 = await lastBlockNumber()
await t.connect(deployer).mint(tokenHolder.address, to1e18(12))
block3 = await lastBlockNumber()
await t.connect(deployer).mint(thirdParty.address, to1e18(13))
})
it("should update current votes", async () => {
expect(await t.getCurrentVotes(tokenHolder.address)).to.equal(0)
expect(await t.getCurrentVotes(thirdParty.address)).to.equal(0)
expect(await t.getCurrentVotes(delegatee.address)).to.equal(
initialBalance.add(to1e18(46))
)
})
it("should keep track of prior votes", async () => {
expect(await t.getPriorVotes(delegatee.address, block1)).to.equal(
initialBalance.add(to1e18(10))
)
expect(await t.getPriorVotes(delegatee.address, block2)).to.equal(
initialBalance.add(to1e18(21))
)
expect(await t.getPriorVotes(delegatee.address, block3)).to.equal(
initialBalance.add(to1e18(33))
)
})
it("should keep track of historic supply", async () => {
expect(await t.getPastTotalSupply(block1 - 1)).to.equal(
initialBalance
)
expect(await t.getPastTotalSupply(block2 - 1)).to.equal(
initialBalance.add(to1e18(10))
)
expect(await t.getPastTotalSupply(block3 - 1)).to.equal(
initialBalance.add(to1e18(21))
)
})
})
})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... and then, I think we can get rid of all other historical supply should be tracked
test cases for minting. All three basic cases are covered (one mint, several mints to one account, several mints to multiple accounts) and we even have delegations mixed-in and not affecting the result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... and I'd do the same for burn
: expand context("when burned several times"
and have one burn, burn from one account, burn from multiple accounts and remove other getPastTotalSupply
checks - we do not really need to distinguish self-delegation or external-delegation, all we care that delegation does not affect the result and we'll have it achieved by these three tests. Plus, we will test more cases specific to getPastTotalSupply
, which is burning from multiple accounts and burning just once.
I am surprised by
Here:
|
`getCurrentVotes` and `getPriorVotes` from T can be substituted by `getVotes` and `getPastVotes` from Checkpoints
Yes, we weren't storing any checkpoints for total supply, which only changes when minting and burning, so this cost increase is expected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks good to me. Want to hear your opinion regarding slither errors: I'd prefer to exclude some detectors or just skip line by line. Otherwise in some moment could be that we will start to fully ignore slither output, I think
uint128[] storage ckpts, | ||
function(uint256, uint256) view returns (uint256) op, | ||
uint256 delta | ||
) internal returns (uint256 oldWeight, uint256 newWeight) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can weights also be uint96
? And op and amounts in other places? or there are issues with that?