Skip to content
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

Merged
merged 14 commits into from
Oct 8, 2021

Conversation

cygnusv
Copy link
Member

@cygnusv 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)

@cygnusv cygnusv requested review from pdyraga and vzotova October 5, 2021 08:59
@cygnusv cygnusv marked this pull request as ready for review October 6, 2021 10:12
* 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
Copy link
Member

@pdyraga pdyraga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work!

contracts/utils/Checkpoints.sol Outdated Show resolved Hide resolved
contracts/utils/Checkpoints.sol Outdated Show resolved Hide resolved
contracts/utils/Checkpoints.sol Outdated Show resolved Hide resolved
contracts/utils/Checkpoints.sol Outdated Show resolved Hide resolved
contracts/utils/Checkpoints.sol Outdated Show resolved Hide resolved
contracts/utils/Checkpoints.sol Outdated Show resolved Hide resolved
contracts/utils/Checkpoints.sol Outdated Show resolved Hide resolved
contracts/utils/Checkpoints.sol Outdated Show resolved Hide resolved
test/governance/Checkpoints.test.js Outdated Show resolved Hide resolved
const block = await mineBlock()
expect(await t.getPastTotalSupply(block - 1)).to.equal(initialBalance)
})
})
Copy link
Member

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))
          )
        })
      })
    })

Copy link
Member

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.

Copy link
Member

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.

@pdyraga
Copy link
Member

pdyraga commented Oct 6, 2021

I am surprised by burn and mint gas cost increase. Any idea what could happen?

main:

······································|····························|·············|······························
|  Methods                                                                                                     │
···················|··················|··············|·············|·············|···············|··············
|  Contract        ·  Method          ·  Min         ·  Max        ·  Avg        ·  # calls      ·  eur (avg)  │
···················|··················|··············|·············|·············|···············|··············
|  T               ·  approve         ·       46880  ·      46916  ·      46903  ·           51  ·          -  │
···················|··················|··············|·············|·············|···············|··············
|  T               ·  burn            ·       39133  ·      73240  ·      68977  ·           16  ·          -  │
···················|··················|··············|·············|·············|···············|··············
|  T               ·  burnFrom        ·           -  ·          -  ·      66521  ·           14  ·          -  │
···················|··················|··············|·············|·············|···············|··············
|  T               ·  delegate        ·       48906  ·     113927  ·      80803  ·           96  ·          -  │
···················|··················|··············|·············|·············|···············|··············
|  T               ·  delegateBySig   ·      112995  ·     127187  ·     124561  ·           22  ·          -  │
···················|··················|··············|·············|·············|···············|··············
|  T               ·  mint            ·       42168  ·     107386  ·      75803  ·          163  ·          -  │
···················|··················|··············|·············|·············|···············|··············
|  T               ·  transfer        ·       42256  ·     139355  ·      78479  ·           85  ·          -  │
···················|··················|··············|·············|·············|···············|··············
|  T               ·  transferFrom    ·       50489  ·     132612  ·     103308  ·           28  ·          -  │
···················|··················|··············|·············|·············|···············|··············

Here:

···························|··················|··············|·············|·············|···············|··············
|  Contract                ·  Method          ·  Min         ·  Max        ·  Avg        ·  # calls      ·  eur (avg)  │
···························|··················|··············|·············|·············|···············|··············
|  T                       ·  approve         ·       46892  ·      46916  ·      46901  ·           65  ·          -  │
···························|··················|··············|·············|·············|···············|··············
|  T                       ·  burn            ·       51456  ·     104086  ·      74929  ·           22  ·          -  │
···························|··················|··············|·············|·············|···············|··············
|  T                       ·  burnFrom        ·       44715  ·      97345  ·      68188  ·           22  ·          -  │
···························|··················|··············|·············|·············|···············|··············
|  T                       ·  delegate        ·       48816  ·      96077  ·      78971  ·          104  ·          -  │
···························|··················|··············|·············|·············|···············|··············
|  T                       ·  delegateBySig   ·       92632  ·     126371  ·     116599  ·           22  ·          -  │
···························|··················|··············|·············|·············|···············|··············
|  T                       ·  mint            ·       54532  ·     121674  ·     114797  ·          193  ·          -  │
···························|··················|··············|·············|·············|···············|··············
|  T                       ·  transfer        ·       42380  ·     118856  ·      70385  ·           85  ·          -  │
···························|··················|··············|·············|·············|···············|··············
|  T                       ·  transferFrom    ·       50569  ·     112069  ·      90129  ·           28  ·          -  │

@cygnusv
Copy link
Member Author

cygnusv commented Oct 7, 2021

I am surprised by burn and mint gas cost increase. Any idea what could happen?

Yes, we weren't storing any checkpoints for total supply, which only changes when minting and burning, so this cost increase is expected.

Copy link
Contributor

@vzotova vzotova left a 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) {
Copy link
Contributor

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants