From fddde561c6fc3667cc2a2c4d8c4ca042c2338a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 21 May 2024 15:42:11 +0200 Subject: [PATCH 01/18] move UDT to its own folder --- .../contracts/{ => tokens/UDT}/UnlockDiscountTokenV3.sol | 0 .../contracts/{ => tokens/UDT}/UnlockProtocolGovernor.sol | 0 .../contracts/{ => tokens/UDT}/UnlockProtocolTimelock.sol | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename smart-contracts/contracts/{ => tokens/UDT}/UnlockDiscountTokenV3.sol (100%) rename smart-contracts/contracts/{ => tokens/UDT}/UnlockProtocolGovernor.sol (100%) rename smart-contracts/contracts/{ => tokens/UDT}/UnlockProtocolTimelock.sol (100%) diff --git a/smart-contracts/contracts/UnlockDiscountTokenV3.sol b/smart-contracts/contracts/tokens/UDT/UnlockDiscountTokenV3.sol similarity index 100% rename from smart-contracts/contracts/UnlockDiscountTokenV3.sol rename to smart-contracts/contracts/tokens/UDT/UnlockDiscountTokenV3.sol diff --git a/smart-contracts/contracts/UnlockProtocolGovernor.sol b/smart-contracts/contracts/tokens/UDT/UnlockProtocolGovernor.sol similarity index 100% rename from smart-contracts/contracts/UnlockProtocolGovernor.sol rename to smart-contracts/contracts/tokens/UDT/UnlockProtocolGovernor.sol diff --git a/smart-contracts/contracts/UnlockProtocolTimelock.sol b/smart-contracts/contracts/tokens/UDT/UnlockProtocolTimelock.sol similarity index 100% rename from smart-contracts/contracts/UnlockProtocolTimelock.sol rename to smart-contracts/contracts/tokens/UDT/UnlockProtocolTimelock.sol From 6f70daed24f7b7f79f22adf6b7733decf14df5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 21 May 2024 16:05:47 +0200 Subject: [PATCH 02/18] OZ contracts to v5 --- smart-contracts/contracts/CardPurchaser.sol | 3 +-- .../contracts/hooks/CaptchaHook.sol | 5 +++-- .../contracts/hooks/DiscountCodeHook.sol | 3 ++- .../contracts/hooks/GitcoinHook.sol | 5 +++-- smart-contracts/contracts/hooks/GuildHook.sol | 5 +++-- .../contracts/hooks/PasswordRequiredHook.sol | 3 ++- .../contracts/test-artifacts/TestERC1155.sol | 1 - .../contracts/test-artifacts/TestERC721.sol | 8 +++---- .../test-artifacts/TestProxyAdmin.sol | 4 +++- smart-contracts/package.json | 4 +++- yarn.lock | 22 +++++++++++++++++-- 11 files changed, 43 insertions(+), 20 deletions(-) diff --git a/smart-contracts/contracts/CardPurchaser.sol b/smart-contracts/contracts/CardPurchaser.sol index e352fbaa09c..f0acc6715a9 100644 --- a/smart-contracts/contracts/CardPurchaser.sol +++ b/smart-contracts/contracts/CardPurchaser.sol @@ -54,12 +54,11 @@ contract CardPurchaser is Ownable, EIP712 { address _owner, address _unlockAddress, address _usdc - ) EIP712("Card Purchaser", "1") Ownable() { + ) EIP712("Card Purchaser", "1") Ownable(_owner) { name = "Card Purchaser"; version = "1"; unlockAddress = _unlockAddress; usdc = _usdc; - transferOwnership(_owner); } /** diff --git a/smart-contracts/contracts/hooks/CaptchaHook.sol b/smart-contracts/contracts/hooks/CaptchaHook.sol index ce52eb63557..7911b87740a 100644 --- a/smart-contracts/contracts/hooks/CaptchaHook.sol +++ b/smart-contracts/contracts/hooks/CaptchaHook.sol @@ -1,6 +1,7 @@ //SPDX-License-Identifier: Unlicense pragma solidity ^0.8.0; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@unlock-protocol/contracts/dist/PublicLock/IPublicLockV12.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; @@ -8,7 +9,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; contract CaptchaHook is Ownable { mapping(address => bool) public signers; - constructor() {} + constructor() Ownable(msg.sender) {} function addSigner(address signer) public onlyOwner { signers[signer] = true; @@ -44,7 +45,7 @@ contract CaptchaHook is Ownable { ) public view returns (bool isSigner) { bytes memory encoded = abi.encodePacked(message); bytes32 messageHash = keccak256(encoded); - bytes32 hash = ECDSA.toEthSignedMessageHash(messageHash); + bytes32 hash = MessageHashUtils.toEthSignedMessageHash(messageHash); address recoveredAddress = ECDSA.recover(hash, signature); return signers[recoveredAddress]; } diff --git a/smart-contracts/contracts/hooks/DiscountCodeHook.sol b/smart-contracts/contracts/hooks/DiscountCodeHook.sol index 4e1ca1939b6..045bdbc4acf 100644 --- a/smart-contracts/contracts/hooks/DiscountCodeHook.sol +++ b/smart-contracts/contracts/hooks/DiscountCodeHook.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.9; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@unlock-protocol/contracts/dist/PublicLock/IPublicLockV12.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; error TOO_BIG(); error NOT_AUTHORIZED(); @@ -86,7 +87,7 @@ contract DiscountHook { bytes calldata signature ) public pure returns (address recoveredAddress) { bytes32 hash = keccak256(abi.encodePacked(message)); - bytes32 signedMessageHash = ECDSA.toEthSignedMessageHash(hash); + bytes32 signedMessageHash = MessageHashUtils.toEthSignedMessageHash(hash); return ECDSA.recover(signedMessageHash, signature); } diff --git a/smart-contracts/contracts/hooks/GitcoinHook.sol b/smart-contracts/contracts/hooks/GitcoinHook.sol index daec0ed1242..b7f0be80f39 100644 --- a/smart-contracts/contracts/hooks/GitcoinHook.sol +++ b/smart-contracts/contracts/hooks/GitcoinHook.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import "@unlock-protocol/contracts/dist/PublicLock/IPublicLockV13.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract GitcoinHook is Ownable { mapping(address => bool) public signers; - constructor() {} + constructor() Ownable(msg.sender) {} function addSigner(address signer) public onlyOwner { signers[signer] = true; @@ -43,7 +44,7 @@ contract GitcoinHook is Ownable { ) private view returns (bool isSigner) { bytes memory encoded = abi.encodePacked(message); bytes32 messageHash = keccak256(encoded); - bytes32 hash = ECDSA.toEthSignedMessageHash(messageHash); + bytes32 hash = MessageHashUtils.toEthSignedMessageHash(messageHash); address recoveredAddress = ECDSA.recover(hash, signature); return signers[recoveredAddress]; } diff --git a/smart-contracts/contracts/hooks/GuildHook.sol b/smart-contracts/contracts/hooks/GuildHook.sol index 389e4b7d95e..45def4c5cbb 100644 --- a/smart-contracts/contracts/hooks/GuildHook.sol +++ b/smart-contracts/contracts/hooks/GuildHook.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import "@unlock-protocol/contracts/dist/PublicLock/IPublicLockV13.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract GuildHook is Ownable { mapping(address => bool) public signers; - constructor() {} + constructor() Ownable(msg.sender) {} function addSigner(address signer) public onlyOwner { signers[signer] = true; @@ -44,7 +45,7 @@ contract GuildHook is Ownable { ) private view returns (bool isSigner) { bytes memory encoded = abi.encodePacked(message); bytes32 messageHash = keccak256(encoded); - bytes32 hash = ECDSA.toEthSignedMessageHash(messageHash); + bytes32 hash = MessageHashUtils.toEthSignedMessageHash(messageHash); address recoveredAddress = ECDSA.recover(hash, signature); return signers[recoveredAddress]; } diff --git a/smart-contracts/contracts/hooks/PasswordRequiredHook.sol b/smart-contracts/contracts/hooks/PasswordRequiredHook.sol index bc85f0905bd..43a438723f8 100644 --- a/smart-contracts/contracts/hooks/PasswordRequiredHook.sol +++ b/smart-contracts/contracts/hooks/PasswordRequiredHook.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@unlock-protocol/contracts/dist/PublicLock/IPublicLockV12.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; error WRONG_PASSWORD(); error NOT_AUTHORIZED(); @@ -52,7 +53,7 @@ contract PasswordRequiredHook { bytes calldata signature ) public pure returns (address recoveredAddress) { bytes32 hash = keccak256(abi.encodePacked(message)); - bytes32 signedMessageHash = ECDSA.toEthSignedMessageHash(hash); + bytes32 signedMessageHash = MessageHashUtils.toEthSignedMessageHash(hash); return ECDSA.recover(signedMessageHash, signature); } diff --git a/smart-contracts/contracts/test-artifacts/TestERC1155.sol b/smart-contracts/contracts/test-artifacts/TestERC1155.sol index 2da8f741f40..48a59c94f91 100644 --- a/smart-contracts/contracts/test-artifacts/TestERC1155.sol +++ b/smart-contracts/contracts/test-artifacts/TestERC1155.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; -import "@openzeppelin/contracts/utils/Counters.sol"; contract TestERC1155 is ERC1155 { constructor() ERC1155("tokenURIExample") {} diff --git a/smart-contracts/contracts/test-artifacts/TestERC721.sol b/smart-contracts/contracts/test-artifacts/TestERC721.sol index 38b608f9d65..51f0d25eb0d 100644 --- a/smart-contracts/contracts/test-artifacts/TestERC721.sol +++ b/smart-contracts/contracts/test-artifacts/TestERC721.sol @@ -2,18 +2,16 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/utils/Counters.sol"; contract TestERC721 is ERC721 { - using Counters for Counters.Counter; - Counters.Counter private _tokenIds; + uint private _lastTokenId = 1; constructor() ERC721("BasicToken", "BASIC") {} function mint(address holder) public returns (uint256) { - _tokenIds.increment(); + _lastTokenId++; - uint256 newItemId = _tokenIds.current(); + uint256 newItemId = _lastTokenId; _mint(holder, newItemId); return newItemId; diff --git a/smart-contracts/contracts/test-artifacts/TestProxyAdmin.sol b/smart-contracts/contracts/test-artifacts/TestProxyAdmin.sol index f98da9b7b23..6250451b367 100644 --- a/smart-contracts/contracts/test-artifacts/TestProxyAdmin.sol +++ b/smart-contracts/contracts/test-artifacts/TestProxyAdmin.sol @@ -3,4 +3,6 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -contract TestProxyAdmin is ProxyAdmin {} +contract TestProxyAdmin is ProxyAdmin { + constructor() ProxyAdmin(msg.sender) {} +} diff --git a/smart-contracts/package.json b/smart-contracts/package.json index 4446ce55e31..67fcdbe735e 100644 --- a/smart-contracts/package.json +++ b/smart-contracts/package.json @@ -12,8 +12,10 @@ "@nomicfoundation/hardhat-ethers": "3.0.5", "@nomicfoundation/hardhat-network-helpers": "1.0.10", "@nomicfoundation/hardhat-verify": "2.0.6", - "@openzeppelin/contracts": "4.9.6", + "@openzeppelin/contracts": "5.0.2", "@openzeppelin/contracts-upgradeable": "4.9.6", + "@openzeppelin/contracts-upgradeable5": "npm:@openzeppelin/contracts-upgradeable@5.0.2", + "@openzeppelin/contracts5": "npm:@openzeppelin/contracts@5.0.2", "@openzeppelin/hardhat-upgrades": "3.1.0", "@openzeppelin/upgrades-core": "1.33.1", "@safe-global/safe-core-sdk": "3.3.5", diff --git a/yarn.lock b/yarn.lock index 282cd886b71..65aa89ae142 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12684,6 +12684,15 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts-upgradeable5@npm:@openzeppelin/contracts-upgradeable@5.0.2": + version: 5.0.2 + resolution: "@openzeppelin/contracts-upgradeable@npm:5.0.2" + peerDependencies: + "@openzeppelin/contracts": 5.0.2 + checksum: 10/71847c6bbd7a859a2f02f496215b9664e41375589010e66da32f080d9af9215accf558da63134926e0eb3eb87ee7ab952462bc877ec5c5e1ac077b44cac9c363 + languageName: node + linkType: hard + "@openzeppelin/contracts-upgradeable@npm:4.5.2": version: 4.5.2 resolution: "@openzeppelin/contracts-upgradeable@npm:4.5.2" @@ -12705,6 +12714,13 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts5@npm:@openzeppelin/contracts@5.0.2, @openzeppelin/contracts@npm:5.0.2": + version: 5.0.2 + resolution: "@openzeppelin/contracts@npm:5.0.2" + checksum: 10/938ebffbdade7dc59ea3df5b562c0e457bbefde9d82be8fa2acfd11da887df11653ac07922f41746b80cdbc106430e1e6978ce244fe99b00a7d9dc1418fc7670 + languageName: node + linkType: hard + "@openzeppelin/contracts@npm:3.4.1-solc-0.7-2": version: 3.4.1-solc-0.7-2 resolution: "@openzeppelin/contracts@npm:3.4.1-solc-0.7-2" @@ -12740,7 +12756,7 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts@npm:4.9.6, @openzeppelin/contracts@npm:^4.9.2": +"@openzeppelin/contracts@npm:^4.9.2": version: 4.9.6 resolution: "@openzeppelin/contracts@npm:4.9.6" checksum: 10/71f45ad42e68c0559be4ba502115462a01c76fc805c08d3005c10b5550a093f1a2b00b2d7e9d6d1f331e147c50fd4ad832f71c4470ec5b34f5a2d0751cd19a47 @@ -19838,8 +19854,10 @@ __metadata: "@nomicfoundation/hardhat-ethers": "npm:3.0.5" "@nomicfoundation/hardhat-network-helpers": "npm:1.0.10" "@nomicfoundation/hardhat-verify": "npm:2.0.6" - "@openzeppelin/contracts": "npm:4.9.6" + "@openzeppelin/contracts": "npm:5.0.2" "@openzeppelin/contracts-upgradeable": "npm:4.9.6" + "@openzeppelin/contracts-upgradeable5": "npm:@openzeppelin/contracts-upgradeable@5.0.2" + "@openzeppelin/contracts5": "npm:@openzeppelin/contracts@5.0.2" "@openzeppelin/hardhat-upgrades": "npm:3.1.0" "@openzeppelin/upgrades-core": "npm:1.33.1" "@safe-global/safe-core-sdk": "npm:3.3.5" From 6b4af586d012e9b5e10cd26afdc21ec8332ba4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 21 May 2024 16:09:41 +0200 Subject: [PATCH 03/18] UP token draft --- .../tokens/UP/UnlockProtocolToken.sol | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol diff --git a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol b/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol new file mode 100644 index 00000000000..aff3435c0fb --- /dev/null +++ b/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable5/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/proxy/utils/Initializable.sol"; + +contract UnlockProtocolToken is + Initializable, + ERC20Upgradeable, + ERC20PermitUpgradeable, + OwnableUpgradeable +{ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address initialOwner) public initializer { + __ERC20_init("UnlockProtocolToken", "UP"); + __ERC20Permit_init("UnlockProtocolToken"); + __Ownable_init(initialOwner); + + // premint the supply + _mint(address(this), 1000000000 * 10 ** decimals()); + } +} From bf5ac5ff8d5bc76b8ce445ecd89dec77ac7d82be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 21 May 2024 16:25:00 +0200 Subject: [PATCH 04/18] test for UP token setttings --- .../tokens/UP/UnlockProtocolToken.sol | 9 ++++- .../UnlockProtocolToken/initialization.js | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 smart-contracts/test/UnlockProtocolToken/initialization.js diff --git a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol b/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol index aff3435c0fb..13cfd0e117e 100644 --- a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol +++ b/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol @@ -13,17 +13,22 @@ contract UnlockProtocolToken is ERC20PermitUpgradeable, OwnableUpgradeable { + uint public constant TOTAL_SUPPLY = 1_000_000_000; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } - function initialize(address initialOwner) public initializer { + function initialize( + address initialOwner, + address preMinter + ) public initializer { __ERC20_init("UnlockProtocolToken", "UP"); __ERC20Permit_init("UnlockProtocolToken"); __Ownable_init(initialOwner); // premint the supply - _mint(address(this), 1000000000 * 10 ** decimals()); + _mint(preMinter, TOTAL_SUPPLY * 10 ** decimals()); } } diff --git a/smart-contracts/test/UnlockProtocolToken/initialization.js b/smart-contracts/test/UnlockProtocolToken/initialization.js new file mode 100644 index 00000000000..b6d7f99f5a0 --- /dev/null +++ b/smart-contracts/test/UnlockProtocolToken/initialization.js @@ -0,0 +1,40 @@ +const { assert } = require('chai') +const { ethers, upgrades } = require('hardhat') + +describe('UnlockProtocolToken / initialization', () => { + let owner, preMinter + let up + before(async () => { + ;[owner, preMinter] = await ethers.getSigners() + + const UP = await ethers.getContractFactory('UnlockProtocolToken') + up = await upgrades.deployProxy(UP, [ + await owner.getAddress(), + await preMinter.getAddress(), + ]) + }) + describe('settings', () => { + it('name is properly set', async () => { + assert.equal(await up.name(), 'UnlockProtocolToken') + }) + it('ticker is properly set', async () => { + assert.equal(await up.symbol(), 'UP') + }) + it('decimal is properly set', async () => { + assert.equal(await up.decimals(), 18) + }) + }) + describe('ownership', () => { + it('is properly set', async () => { + assert.equal(await owner.getAddress(), await up.owner()) + }) + }) + describe('total supply is preminted correctly', () => { + it('amount was transferred', async () => { + assert.equal( + (await up.TOTAL_SUPPLY()) * BigInt(10 ** 18), + await up.balanceOf(await preMinter.getAddress()) + ) + }) + }) +}) From ab8cb902026786c2324a0d454079da9c0fe7a76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 21 May 2024 16:38:12 +0200 Subject: [PATCH 05/18] add votes to erc20 --- .../tokens/UP/UnlockProtocolToken.sol | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol b/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol index 13cfd0e117e..3ae4cd542ef 100644 --- a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol +++ b/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol @@ -1,16 +1,20 @@ // SPDX-License-Identifier: MIT // Compatible with OpenZeppelin Contracts ^5.0.0 -pragma solidity ^0.8.20; +pragma solidity ^0.8.21; import "@openzeppelin/contracts-upgradeable5/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable5/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable5/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable5/proxy/utils/Initializable.sol"; +import {NoncesUpgradeable} from "@openzeppelin/contracts-upgradeable5/utils/NoncesUpgradeable.sol"; +/// @custom:security-contact hello@unlock-protocol.com contract UnlockProtocolToken is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable, + ERC20VotesUpgradeable, OwnableUpgradeable { uint public constant TOTAL_SUPPLY = 1_000_000_000; @@ -26,9 +30,31 @@ contract UnlockProtocolToken is ) public initializer { __ERC20_init("UnlockProtocolToken", "UP"); __ERC20Permit_init("UnlockProtocolToken"); + __ERC20Votes_init(); __Ownable_init(initialOwner); // premint the supply _mint(preMinter, TOTAL_SUPPLY * 10 ** decimals()); } + + // The following functions are overrides required by Solidity. + + function _update( + address from, + address to, + uint256 value + ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._update(from, to, value); + } + + function nonces( + address owner + ) + public + view + override(ERC20PermitUpgradeable, NoncesUpgradeable) + returns (uint256) + { + return super.nonces(owner); + } } From bcb72863affd82443bfd3b6f80d4e1766ed6065a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 21 May 2024 16:50:39 +0200 Subject: [PATCH 06/18] repurpose UDT votes test --- .../test/UnlockDiscountToken/governance.js | 520 ------------------ .../test/UnlockProtocolToken/votes.js | 406 ++++++++++++++ 2 files changed, 406 insertions(+), 520 deletions(-) delete mode 100644 smart-contracts/test/UnlockDiscountToken/governance.js create mode 100644 smart-contracts/test/UnlockProtocolToken/votes.js diff --git a/smart-contracts/test/UnlockDiscountToken/governance.js b/smart-contracts/test/UnlockDiscountToken/governance.js deleted file mode 100644 index 33d1ec4dbb2..00000000000 --- a/smart-contracts/test/UnlockDiscountToken/governance.js +++ /dev/null @@ -1,520 +0,0 @@ -// tests adapted/imported from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7e41bf2259950c33e55604015875b7780b6a2e63/test/token/ERC20/extensions/ERC20VotesComp.test.js -const { ethers } = require('hardhat') -const { assert } = require('chai') -const { advanceBlock, reverts } = require('../helpers') - -const { - ADDRESS_ZERO, - expectEvent, - notExpectEvent, - compareBigNumbers, - compareBigNumberArrays, - getLatestBlock, -} = require('../helpers') - -const supply = BigInt('10000000000000000000000000') - -describe('UDT ERC20VotesComp extension', () => { - let udt - let holderSigner, recipientSigner - let minter, holder, recipient, holderDelegatee, other1, other2 - - beforeEach(async () => { - ;[ - { address: minter }, - holderSigner, - recipientSigner, - { address: holderDelegatee }, - { address: other1 }, - { address: other2 }, - ] = await ethers.getSigners() - ;({ address: holder } = holderSigner) - ;({ address: recipient } = recipientSigner) - - const UnlockDiscountTokenV3 = await ethers.getContractFactory( - 'UnlockDiscountTokenV3' - ) - udt = await UnlockDiscountTokenV3.deploy() - await udt['initialize(address)'](minter) - }) - - describe('Supply', () => { - describe('balanceOf', () => { - it('grants initial supply to minter account', async () => { - await udt.mint(holder, supply) - assert(supply == (await udt.balanceOf(holder))) - }) - }) - it('minting restriction', async () => { - const amount = BigInt('2') ** BigInt('96') - await reverts( - udt.mint(minter, amount), - 'ERC20Votes: total supply risks overflowing votes' - ) - }) - }) - - describe('Delegation', () => { - it('delegation with balance', async () => { - await udt.mint(holder, supply) - assert.equal(await udt.delegates(minter), ADDRESS_ZERO) - const tx = await udt.connect(holderSigner).delegate(holder) - const receipt = await tx.wait() - const { blockNumber } = receipt - - // console.log(events) - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ADDRESS_ZERO, - toDelegate: holder, - }) - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: supply, - }) - - compareBigNumbers(supply, await udt.getCurrentVotes(holder)) - compareBigNumbers('0', await udt.getPriorVotes(holder, blockNumber - 1)) - await advanceBlock() - compareBigNumbers(supply, await udt.getPriorVotes(holder, blockNumber)) - }) - it('delegation without balance', async () => { - assert.equal(await udt.delegates(holder), ADDRESS_ZERO) - - const tx = await udt.connect(holderSigner).delegate(holder) - const receipt = await tx.wait() - - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ADDRESS_ZERO, - toDelegate: holder, - }) - notExpectEvent(receipt, 'DelegateVotesChanged') - - assert.equal(await udt.delegates(holder), holder) - }) - - describe('change delegation', () => { - beforeEach(async () => { - await udt.mint(holder, supply) - await udt.connect(holderSigner).delegate(holder) - }) - - it('call', async () => { - assert.equal(await udt.delegates(holder), holder) - - const tx = await udt.connect(holderSigner).delegate(holderDelegatee) - const receipt = await tx.wait() - const { blockNumber } = receipt - - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }) - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: '0', - }) - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: supply, - }) - - assert.equal(await udt.delegates(holder), holderDelegatee) - compareBigNumbers('0', await udt.getCurrentVotes(holder)) - compareBigNumbers(supply, await udt.getCurrentVotes(holderDelegatee)) - - compareBigNumbers( - supply, - await udt.getPriorVotes(holder, blockNumber - 1) - ) - - compareBigNumbers( - '0', - await udt.getPriorVotes(holderDelegatee, blockNumber - 1) - ) - - await advanceBlock() - compareBigNumbers('0', await udt.getPriorVotes(holder, blockNumber)) - - compareBigNumbers( - supply, - await udt.getPriorVotes(holderDelegatee, blockNumber) - ) - }) - }) - }) - - describe('Transfers', () => { - let holderVotes - let recipientVotes - - beforeEach(async () => { - await udt.mint(holder, supply) - }) - it('no delegation', async () => { - const tx = await udt.connect(holderSigner).transfer(recipient, 1) - const receipt = await tx.wait() - expectEvent(receipt, 'Transfer', { - from: holder, - to: recipient, - value: '1', - }) - notExpectEvent(receipt, 'DelegateVotesChanged') - - holderVotes = '0' - recipientVotes = '0' - }) - - it('sender delegation', async () => { - await udt.connect(holderSigner).delegate(holder) - - const tx = await udt.connect(holderSigner).transfer(recipient, 1) - const receipt = await tx.wait() - expectEvent(receipt, 'Transfer', { - from: holder, - to: recipient, - value: '1', - }) - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply - 1n, - }) - - holderVotes = supply - 1n - recipientVotes = '0' - }) - - it('receiver delegation', async () => { - await udt.connect(recipientSigner).delegate(recipient) - - const tx = await udt.connect(holderSigner).transfer(recipient, 1) - const receipt = await tx.wait() - expectEvent(receipt, 'Transfer', { - from: holder, - to: recipient, - value: '1', - }) - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: recipient, - previousBalance: '0', - newBalance: '1', - }) - - holderVotes = '0' - recipientVotes = '1' - }) - - it('full delegation', async () => { - await udt.connect(holderSigner).delegate(holder) - await udt.connect(recipientSigner).delegate(recipient) - - const tx = await udt.connect(holderSigner).transfer(recipient, 1) - const receipt = await tx.wait() - expectEvent(receipt, 'Transfer', { - from: holder, - to: recipient, - value: '1', - }) - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply - 1n, - }) - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: recipient, - previousBalance: '0', - newBalance: '1', - }) - - holderVotes = supply - 1n - recipientVotes = '1' - }) - - afterEach(async () => { - compareBigNumbers(holderVotes, await udt.getCurrentVotes(holder)) - compareBigNumbers(recipientVotes, await udt.getCurrentVotes(recipient)) - - // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes" - const blockNumber = await getLatestBlock() - await advanceBlock() - compareBigNumbers( - holderVotes, - await udt.getPriorVotes(holder, blockNumber) - ) - compareBigNumbers( - recipientVotes, - await udt.getPriorVotes(recipient, blockNumber) - ) - }) - }) - - describe('Compound test suite', () => { - beforeEach(async () => { - await udt.mint(holder, supply) - }) - - describe('balanceOf', () => { - it('grants to initial account', async () => { - assert.equal(await udt.balanceOf(holder), '10000000000000000000000000') - }) - }) - - describe('numCheckpoints', () => { - it('returns the number of checkpoints for a delegate', async () => { - await udt.connect(holderSigner).transfer(recipient, '100') // give an account a few tokens for readability - compareBigNumbers('0', await udt.numCheckpoints(other1)) - - const t1 = await udt.connect(recipientSigner).delegate(other1) - compareBigNumbers('1', await udt.numCheckpoints(other1)) - - const t2 = await udt.connect(recipientSigner).transfer(other2, 10) - compareBigNumbers('2', await udt.numCheckpoints(other1)) - - const t3 = await udt.connect(recipientSigner).transfer(other2, 10) - compareBigNumbers('3', await udt.numCheckpoints(other1)) - - const t4 = await udt.connect(holderSigner).transfer(recipient, 20) - compareBigNumbers('4', await udt.numCheckpoints(other1)) - - compareBigNumberArrays(await udt.checkpoints(other1, 0), [ - t1.blockNumber, - '100', - ]) - compareBigNumberArrays(await udt.checkpoints(other1, 1), [ - t2.blockNumber, - '90', - ]) - compareBigNumberArrays(await udt.checkpoints(other1, 2), [ - t3.blockNumber, - '80', - ]) - compareBigNumberArrays(await udt.checkpoints(other1, 3), [ - t4.blockNumber, - '100', - ]) - - await advanceBlock() - compareBigNumbers( - '100', - await udt.getPriorVotes(other1, t1.blockNumber) - ) - - compareBigNumbers('90', await udt.getPriorVotes(other1, t2.blockNumber)) - - compareBigNumbers('80', await udt.getPriorVotes(other1, t3.blockNumber)) - - compareBigNumbers( - '100', - await udt.getPriorVotes(other1, t4.blockNumber) - ) - }) - }) - - describe('getPriorVotes', () => { - it('reverts if block number >= current block', async () => { - await reverts( - udt.getPriorVotes(other1, 5e10), - 'ERC20Votes: block not yet mined' - ) - }) - - it('returns 0 if there are no checkpoints', async () => { - compareBigNumbers('0', await udt.getPriorVotes(other1, 0)) - }) - - it('returns the latest block if >= last checkpoint block', async () => { - const t1 = await udt.connect(holderSigner).delegate(other1) - const { blockNumber } = await t1.wait() - await advanceBlock() - await advanceBlock() - - compareBigNumbers( - '10000000000000000000000000', - await udt.getPriorVotes(other1, blockNumber) - ) - - compareBigNumbers( - '10000000000000000000000000', - await udt.getPriorVotes(other1, blockNumber + 1) - ) - }) - - it('returns zero if < first checkpoint block', async () => { - await advanceBlock() - const t1 = await udt.connect(holderSigner).delegate(other1) - const { blockNumber } = await t1.wait() - await advanceBlock() - await advanceBlock() - - compareBigNumbers('0', await udt.getPriorVotes(other1, blockNumber - 1)) - - compareBigNumbers( - '10000000000000000000000000', - await udt.getPriorVotes(other1, blockNumber + 1) - ) - }) - - it('generally returns the voting balance at the appropriate checkpoint', async () => { - const t1 = await udt.connect(holderSigner).delegate(other1) - await advanceBlock() - await advanceBlock() - const t2 = await udt.connect(holderSigner).transfer(other2, 10) - await advanceBlock() - await advanceBlock() - const t3 = await udt.connect(holderSigner).transfer(other2, 10) - await advanceBlock() - await advanceBlock() - const t4 = await udt - .connect(await ethers.getSigner(other2)) - .transfer(holder, 20) - await advanceBlock() - await advanceBlock() - - compareBigNumbers( - '0', - await udt.getPriorVotes(other1, t1.blockNumber - 1) - ) - compareBigNumbers( - '10000000000000000000000000', - await udt.getPriorVotes(other1, t1.blockNumber) - ) - compareBigNumbers( - '10000000000000000000000000', - await udt.getPriorVotes(other1, t1.blockNumber + 1) - ) - compareBigNumbers( - '9999999999999999999999990', - await udt.getPriorVotes(other1, t2.blockNumber) - ) - compareBigNumbers( - '9999999999999999999999990', - await udt.getPriorVotes(other1, t2.blockNumber + 1) - ) - compareBigNumbers( - '9999999999999999999999980', - await udt.getPriorVotes(other1, t3.blockNumber) - ) - compareBigNumbers( - '9999999999999999999999980', - await udt.getPriorVotes(other1, t3.blockNumber + 1) - ) - compareBigNumbers( - '10000000000000000000000000', - await udt.getPriorVotes(other1, t4.blockNumber) - ) - compareBigNumbers( - '10000000000000000000000000', - await udt.getPriorVotes(other1, t4.blockNumber + 1) - ) - }) - }) - }) - - describe('getPastTotalSupply', () => { - beforeEach(async () => { - await udt.connect(holderSigner).delegate(holder) - }) - - it('reverts if block number >= current block', async () => { - await reverts( - udt.getPastTotalSupply(5e10), - 'ERC20Votes: block not yet mined' - ) - }) - - it('returns 0 if there are no checkpoints', async () => { - compareBigNumbers('0', await udt.getPastTotalSupply(0)) - }) - - it('returns the latest block if >= last checkpoint block', async () => { - const t1 = await udt.mint(holder, supply) - - await advanceBlock() - await advanceBlock() - - compareBigNumbers(supply, await udt.getPastTotalSupply(t1.blockNumber)) - - compareBigNumbers( - supply, - await udt.getPastTotalSupply(t1.blockNumber + 1) - ) - }) - - it('returns zero if < first checkpoint block', async () => { - await advanceBlock() - const t1 = await udt.mint(holder, supply) - await advanceBlock() - await advanceBlock() - - compareBigNumbers('0', await udt.getPastTotalSupply(t1.blockNumber - 1)) - - compareBigNumbers( - '10000000000000000000000000', - await udt.getPastTotalSupply(t1.blockNumber + 1) - ) - }) - - it('generally returns the voting balance at the appropriate checkpoint', async () => { - const t1 = await udt.mint(holder, supply) - await advanceBlock() - await advanceBlock() - const t2 = await udt.mint(holder, 10) - await advanceBlock() - await advanceBlock() - const t3 = await udt.mint(holder, 10) - await advanceBlock() - await advanceBlock() - const t4 = await udt.mint(holder, 20) - await advanceBlock() - await advanceBlock() - - compareBigNumbers('0', await udt.getPastTotalSupply(t1.blockNumber - 1)) - - compareBigNumbers( - '10000000000000000000000000', - await udt.getPastTotalSupply(t1.blockNumber) - ) - - compareBigNumbers( - '10000000000000000000000000', - await udt.getPastTotalSupply(t1.blockNumber + 1) - ) - - compareBigNumbers( - '10000000000000000000000010', - await udt.getPastTotalSupply(t2.blockNumber) - ) - - compareBigNumbers( - '10000000000000000000000010', - await udt.getPastTotalSupply(t2.blockNumber + 1) - ) - - compareBigNumbers( - '10000000000000000000000020', - await udt.getPastTotalSupply(t3.blockNumber) - ) - - compareBigNumbers( - '10000000000000000000000020', - await udt.getPastTotalSupply(t3.blockNumber + 1) - ) - - compareBigNumbers( - '10000000000000000000000040', - await udt.getPastTotalSupply(t4.blockNumber) - ) - - compareBigNumbers( - '10000000000000000000000040', - await udt.getPastTotalSupply(t4.blockNumber + 1) - ) - }) - }) -}) diff --git a/smart-contracts/test/UnlockProtocolToken/votes.js b/smart-contracts/test/UnlockProtocolToken/votes.js new file mode 100644 index 00000000000..a7f7770ca10 --- /dev/null +++ b/smart-contracts/test/UnlockProtocolToken/votes.js @@ -0,0 +1,406 @@ +// tests adapted/imported from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7e41bf2259950c33e55604015875b7780b6a2e63/test/token/ERC20/extensions/ERC20VotesComp.test.js +const { ethers, upgrades } = require('hardhat') +const { assert } = require('chai') +const { advanceBlock, reverts } = require('../helpers') + +const { + ADDRESS_ZERO, + expectEvent, + notExpectEvent, + compareBigNumbers, + compareBigNumberArrays, + getLatestBlock, +} = require('../helpers') + +const supply = BigInt('100000000') + +describe('UnlockProtocolToken / Votes', () => { + let up + let transferToken + let holderSigner, recipientSigner + let owner, minter, holder, recipient, holderDelegatee, other1, other2 + + beforeEach(async () => { + ;[ + { address: owner }, + minter, + holderSigner, + recipientSigner, + { address: holderDelegatee }, + { address: other1 }, + { address: other2 }, + ] = await ethers.getSigners() + ;({ address: holder } = holderSigner) + ;({ address: recipient } = recipientSigner) + + const UnlockDiscountTokenV3 = await ethers.getContractFactory( + 'UnlockProtocolToken' + ) + up = await upgrades.deployProxy(UnlockDiscountTokenV3, [ + owner, + await minter.getAddress(), + ]) + + // helper + transferToken = async (receiver, amount) => { + return await up.connect(minter).transfer(receiver, amount) + } + }) + + describe('Supply', () => { + describe('balanceOf', () => { + it('grants initial supply to minter account', async () => { + await transferToken(holder, supply) + assert(supply == (await up.balanceOf(holder))) + }) + }) + // it('minting restriction', async () => { + // const amount = BigInt('2') ** BigInt('96') + // await reverts( + // up.mint(minter, amount), + // 'ERC20Votes: total supply risks overflowing votes' + // ) + // }) + }) + + describe('Delegation', () => { + it('delegation with balance', async () => { + await transferToken(holder, supply) + assert.equal(await up.delegates(minter), ADDRESS_ZERO) + const tx = await up.connect(holderSigner).delegate(holder) + const receipt = await tx.wait() + const { blockNumber } = receipt + + // console.log(events) + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ADDRESS_ZERO, + toDelegate: holder, + }) + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }) + + compareBigNumbers(supply, await up.getVotes(holder)) + compareBigNumbers('0', await up.getPastVotes(holder, blockNumber - 1)) + await advanceBlock() + compareBigNumbers(supply, await up.getPastVotes(holder, blockNumber)) + }) + it('delegation without balance', async () => { + assert.equal(await up.delegates(holder), ADDRESS_ZERO) + + const tx = await up.connect(holderSigner).delegate(holder) + const receipt = await tx.wait() + + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ADDRESS_ZERO, + toDelegate: holder, + }) + notExpectEvent(receipt, 'DelegateVotesChanged') + + assert.equal(await up.delegates(holder), holder) + }) + + describe('change delegation', () => { + beforeEach(async () => { + await transferToken(holder, supply) + await up.connect(holderSigner).delegate(holder) + }) + + it('call', async () => { + assert.equal(await up.delegates(holder), holder) + + const tx = await up.connect(holderSigner).delegate(holderDelegatee) + const receipt = await tx.wait() + const { blockNumber } = receipt + + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }) + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }) + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }) + + assert.equal(await up.delegates(holder), holderDelegatee) + compareBigNumbers('0', await up.getVotes(holder)) + compareBigNumbers(supply, await up.getVotes(holderDelegatee)) + + compareBigNumbers( + supply, + await up.getPastVotes(holder, blockNumber - 1) + ) + + compareBigNumbers( + '0', + await up.getPastVotes(holderDelegatee, blockNumber - 1) + ) + + await advanceBlock() + compareBigNumbers('0', await up.getPastVotes(holder, blockNumber)) + + compareBigNumbers( + supply, + await up.getPastVotes(holderDelegatee, blockNumber) + ) + }) + }) + }) + + describe('Transfers', () => { + let holderVotes + let recipientVotes + + beforeEach(async () => { + await transferToken(holder, supply) + }) + it('no delegation', async () => { + const tx = await up.connect(holderSigner).transfer(recipient, 1) + const receipt = await tx.wait() + expectEvent(receipt, 'Transfer', { + from: holder, + to: recipient, + value: '1', + }) + notExpectEvent(receipt, 'DelegateVotesChanged') + + holderVotes = '0' + recipientVotes = '0' + }) + + it('sender delegation', async () => { + await up.connect(holderSigner).delegate(holder) + + const tx = await up.connect(holderSigner).transfer(recipient, 1) + const receipt = await tx.wait() + expectEvent(receipt, 'Transfer', { + from: holder, + to: recipient, + value: '1', + }) + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: supply - 1n, + }) + + holderVotes = supply - 1n + recipientVotes = '0' + }) + + it('receiver delegation', async () => { + await up.connect(recipientSigner).delegate(recipient) + + const tx = await up.connect(holderSigner).transfer(recipient, 1) + const receipt = await tx.wait() + expectEvent(receipt, 'Transfer', { + from: holder, + to: recipient, + value: '1', + }) + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: recipient, + previousBalance: '0', + newBalance: '1', + }) + + holderVotes = '0' + recipientVotes = '1' + }) + + it('full delegation', async () => { + await up.connect(holderSigner).delegate(holder) + await up.connect(recipientSigner).delegate(recipient) + + const tx = await up.connect(holderSigner).transfer(recipient, 1) + const receipt = await tx.wait() + expectEvent(receipt, 'Transfer', { + from: holder, + to: recipient, + value: '1', + }) + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: supply - 1n, + }) + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: recipient, + previousBalance: '0', + newBalance: '1', + }) + + holderVotes = supply - 1n + recipientVotes = '1' + }) + + afterEach(async () => { + compareBigNumbers(holderVotes, await up.getVotes(holder)) + compareBigNumbers(recipientVotes, await up.getVotes(recipient)) + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await getLatestBlock() + await advanceBlock() + compareBigNumbers(holderVotes, await up.getPastVotes(holder, blockNumber)) + compareBigNumbers( + recipientVotes, + await up.getPastVotes(recipient, blockNumber) + ) + }) + }) + + describe('Compound test suite', () => { + beforeEach(async () => { + await transferToken(holder, supply) + }) + + describe('balanceOf', () => { + it('grants to initial account', async () => { + assert.equal(await up.balanceOf(holder), supply) + }) + }) + + describe('numCheckpoints', () => { + it('returns the number of checkpoints for a delegate', async () => { + await up.connect(holderSigner).transfer(recipient, '100') // give an account a few tokens for readability + compareBigNumbers('0', await up.numCheckpoints(other1)) + + const t1 = await up.connect(recipientSigner).delegate(other1) + compareBigNumbers('1', await up.numCheckpoints(other1)) + + const t2 = await up.connect(recipientSigner).transfer(other2, 10) + compareBigNumbers('2', await up.numCheckpoints(other1)) + + const t3 = await up.connect(recipientSigner).transfer(other2, 10) + compareBigNumbers('3', await up.numCheckpoints(other1)) + + const t4 = await up.connect(holderSigner).transfer(recipient, 20) + compareBigNumbers('4', await up.numCheckpoints(other1)) + + compareBigNumberArrays(await up.checkpoints(other1, 0), [ + t1.blockNumber, + '100', + ]) + compareBigNumberArrays(await up.checkpoints(other1, 1), [ + t2.blockNumber, + '90', + ]) + compareBigNumberArrays(await up.checkpoints(other1, 2), [ + t3.blockNumber, + '80', + ]) + compareBigNumberArrays(await up.checkpoints(other1, 3), [ + t4.blockNumber, + '100', + ]) + + await advanceBlock() + compareBigNumbers('100', await up.getPastVotes(other1, t1.blockNumber)) + + compareBigNumbers('90', await up.getPastVotes(other1, t2.blockNumber)) + + compareBigNumbers('80', await up.getPastVotes(other1, t3.blockNumber)) + + compareBigNumbers('100', await up.getPastVotes(other1, t4.blockNumber)) + }) + }) + + describe('getPastVotes', () => { + it('reverts if block number >= current block', async () => { + await reverts(up.getPastVotes(other1, 5e10), 'ERC5805FutureLookup') + }) + + it('returns 0 if there are no checkpoints', async () => { + compareBigNumbers('0', await up.getPastVotes(other1, 0)) + }) + + it('returns the latest block if >= last checkpoint block', async () => { + const t1 = await up.connect(holderSigner).delegate(other1) + const { blockNumber } = await t1.wait() + await advanceBlock() + await advanceBlock() + + compareBigNumbers(supply, await up.getPastVotes(other1, blockNumber)) + + compareBigNumbers( + supply, + await up.getPastVotes(other1, blockNumber + 1) + ) + }) + + it('returns zero if < first checkpoint block', async () => { + await advanceBlock() + const t1 = await up.connect(holderSigner).delegate(other1) + const { blockNumber } = await t1.wait() + await advanceBlock() + await advanceBlock() + + compareBigNumbers('0', await up.getPastVotes(other1, blockNumber - 1)) + + compareBigNumbers( + supply, + await up.getPastVotes(other1, blockNumber + 1) + ) + }) + + it('generally returns the voting balance at the appropriate checkpoint', async () => { + const t1 = await up.connect(holderSigner).delegate(other1) + await advanceBlock() + await advanceBlock() + const t2 = await up.connect(holderSigner).transfer(other2, 10) + await advanceBlock() + await advanceBlock() + const t3 = await up.connect(holderSigner).transfer(other2, 10) + await advanceBlock() + await advanceBlock() + const t4 = await up + .connect(await ethers.getSigner(other2)) + .transfer(holder, 20) + await advanceBlock() + await advanceBlock() + + compareBigNumbers( + '0', + await up.getPastVotes(other1, t1.blockNumber - 1) + ) + compareBigNumbers(supply, await up.getPastVotes(other1, t1.blockNumber)) + compareBigNumbers( + supply, + await up.getPastVotes(other1, t1.blockNumber + 1) + ) + compareBigNumbers( + '99999990', + await up.getPastVotes(other1, t2.blockNumber) + ) + compareBigNumbers( + '99999990', + await up.getPastVotes(other1, t2.blockNumber + 1) + ) + compareBigNumbers( + '99999980', + await up.getPastVotes(other1, t3.blockNumber) + ) + compareBigNumbers( + '99999980', + await up.getPastVotes(other1, t3.blockNumber + 1) + ) + compareBigNumbers(supply, await up.getPastVotes(other1, t4.blockNumber)) + compareBigNumbers( + supply, + await up.getPastVotes(other1, t4.blockNumber + 1) + ) + }) + }) + }) +}) From 618a91ea7c617181e65ee7ac1a883a471c210bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 21 May 2024 17:38:56 +0200 Subject: [PATCH 07/18] gov + timelock --- .../contracts/tokens/UP/UPGovernor.sol | 166 ++++++++++++++++++ .../contracts/tokens/UP/UPTimelock.sol | 14 ++ .../governor.js | 38 ++-- 3 files changed, 196 insertions(+), 22 deletions(-) create mode 100644 smart-contracts/contracts/tokens/UP/UPGovernor.sol create mode 100644 smart-contracts/contracts/tokens/UP/UPTimelock.sol rename smart-contracts/test/{UnlockDiscountToken => UnlockProtocolToken}/governor.js (88%) diff --git a/smart-contracts/contracts/tokens/UP/UPGovernor.sol b/smart-contracts/contracts/tokens/UP/UPGovernor.sol new file mode 100644 index 00000000000..8472cf44c19 --- /dev/null +++ b/smart-contracts/contracts/tokens/UP/UPGovernor.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts-upgradeable5/governance/GovernorUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorSettingsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorCountingSimpleUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorVotesUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorTimelockControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/proxy/utils/Initializable.sol"; + +/// @custom:security-contact hello@unlock-protocol.com +contract UPGovernor is + Initializable, + GovernorUpgradeable, + GovernorSettingsUpgradeable, + GovernorCountingSimpleUpgradeable, + GovernorVotesUpgradeable, + GovernorTimelockControlUpgradeable +{ + uint private _quorum; + + // add custom event for quorum changes + event QuorumSet(uint oldVotingDelay, uint newVotingDelay); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + IVotes _token, + TimelockControllerUpgradeable _timelock + ) public initializer { + __Governor_init("UnlockProtocolGovernor"); + __GovernorSettings_init(43200 /* 6 day */, 43200 /* 6 days */, 0); + __GovernorCountingSimple_init(); + __GovernorVotes_init(_token); + __GovernorTimelockControl_init(_timelock); + + // default quorum set to 30k + _quorum = 30000e18; + } + + // quorum set to 30k + function quorum(uint256) public view override returns (uint256) { + return _quorum; + } + + // helper to change quorum + function setQuorum(uint256 newQuorum) public onlyGovernance { + uint256 oldQuorum = _quorum; + _quorum = newQuorum; + emit QuorumSet(oldQuorum, newQuorum); + } + + // The following functions are overrides required by Solidity. + + function votingDelay() + public + view + override(GovernorUpgradeable, GovernorSettingsUpgradeable) + returns (uint256) + { + return super.votingDelay(); + } + + function votingPeriod() + public + view + override(GovernorUpgradeable, GovernorSettingsUpgradeable) + returns (uint256) + { + return super.votingPeriod(); + } + + function state( + uint256 proposalId + ) + public + view + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) + returns (ProposalState) + { + return super.state(proposalId); + } + + function proposalNeedsQueuing( + uint256 proposalId + ) + public + view + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) + returns (bool) + { + return super.proposalNeedsQueuing(proposalId); + } + + function proposalThreshold() + public + view + override(GovernorUpgradeable, GovernorSettingsUpgradeable) + returns (uint256) + { + return super.proposalThreshold(); + } + + function _queueOperations( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) + internal + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) + returns (uint48) + { + return + super._queueOperations( + proposalId, + targets, + values, + calldatas, + descriptionHash + ); + } + + function _executeOperations( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) { + super._executeOperations( + proposalId, + targets, + values, + calldatas, + descriptionHash + ); + } + + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) + internal + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) + returns (uint256) + { + return super._cancel(targets, values, calldatas, descriptionHash); + } + + function _executor() + internal + view + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) + returns (address) + { + return super._executor(); + } +} diff --git a/smart-contracts/contracts/tokens/UP/UPTimelock.sol b/smart-contracts/contracts/tokens/UP/UPTimelock.sol new file mode 100644 index 00000000000..0278a6e4838 --- /dev/null +++ b/smart-contracts/contracts/tokens/UP/UPTimelock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; + +contract UPTimelock is TimelockControllerUpgradeable { + function initialize( + uint256 minDelay, + address[] memory proposers, + address[] memory executors + ) public initializer { + __TimelockController_init(minDelay, proposers, executors, msg.sender); + } +} diff --git a/smart-contracts/test/UnlockDiscountToken/governor.js b/smart-contracts/test/UnlockProtocolToken/governor.js similarity index 88% rename from smart-contracts/test/UnlockDiscountToken/governor.js rename to smart-contracts/test/UnlockProtocolToken/governor.js index 0910407f6cf..ff8541113cd 100644 --- a/smart-contracts/test/UnlockDiscountToken/governor.js +++ b/smart-contracts/test/UnlockProtocolToken/governor.js @@ -12,16 +12,17 @@ const { getEvent } = require('@unlock-protocol/hardhat-helpers') const PROPOSER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('PROPOSER_ROLE')) -describe('UnlockProtocolGovernor', () => { +// default values +const SIX_DAYS = 43200 // in blocks +const votingDelay = SIX_DAYS // +const votingPeriod = SIX_DAYS +const defaultQuorum = BigInt('30000') * BigInt(10 ** 18) + +describe('UP Governor & Timelock', () => { let gov let udt let updateTx - // default values - const votingDelay = 1 - const votingPeriod = 45818 - const defaultQuorum = ethers.parseEther('15000') - // helper to recreate voting process const launchVotingProcess = async (voter, proposal) => { const proposalTx = await gov.propose(...proposal) @@ -35,7 +36,7 @@ describe('UnlockProtocolGovernor', () => { // wait for a block (default voting delay) const currentBlock = await ethers.provider.getBlockNumber() - await advanceBlockTo(currentBlock + 2) + await advanceBlock(BigInt(currentBlock + 1) + (await gov.votingDelay())) // now ready to receive votes assert.equal(await gov.state(proposalId), 1) // Active @@ -73,26 +74,19 @@ describe('UnlockProtocolGovernor', () => { beforeEach(async () => { // deploying timelock with a proxy - const UnlockProtocolTimelock = await ethers.getContractFactory( - 'UnlockProtocolTimelock' - ) + const UPTimelock = await ethers.getContractFactory('UPTimelock') - const timelock = await upgrades.deployProxy(UnlockProtocolTimelock, [ + const timelock = await upgrades.deployProxy(UPTimelock, [ 1, // 1 second delay [], // proposers list is empty at deployment [ADDRESS_ZERO], // allow any address to execute a proposal once the timelock has expired ]) // deploy governor - const UnlockProtocolGovernor = await ethers.getContractFactory( - 'UnlockProtocolGovernor' - ) + const UPGovernor = await ethers.getContractFactory('UPGovernor') - gov = await upgrades.deployProxy(UnlockProtocolGovernor, [ + gov = await upgrades.deployProxy(UPGovernor, [ await udt.getAddress(), - votingDelay, - votingPeriod, - defaultQuorum, await timelock.getAddress(), ]) @@ -109,7 +103,7 @@ describe('UnlockProtocolGovernor', () => { assert.equal(await gov.votingPeriod(), votingPeriod) }) - it('quorum is 15k UDT', async () => { + it('quorum is 30k UDT', async () => { assert.equal(await gov.quorum(1), defaultQuorum) }) }) @@ -117,9 +111,9 @@ describe('UnlockProtocolGovernor', () => { describe('Update voting params', () => { it('should only be possible through voting', async () => { assert.equal(await gov.votingDelay(), votingDelay) - await reverts(gov.setVotingDelay(2), 'Governor: onlyGovernance') - await reverts(gov.setQuorum(2), 'Governor: onlyGovernance') - await reverts(gov.setVotingPeriod(2), 'Governor: onlyGovernance') + await reverts(gov.setVotingDelay(2), 'GovernorOnlyExecutor') + await reverts(gov.setQuorum(2), 'GovernorOnlyExecutor') + await reverts(gov.setVotingPeriod(2), 'GovernorOnlyExecutor') }) beforeEach(async () => { From 7f8110c09ead70b900c3bd726d3a676b14071b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 22 May 2024 14:18:20 +0200 Subject: [PATCH 08/18] fix governor settings tests --- .../test/UnlockProtocolToken/governor.js | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/smart-contracts/test/UnlockProtocolToken/governor.js b/smart-contracts/test/UnlockProtocolToken/governor.js index ff8541113cd..41a7fb49eb2 100644 --- a/smart-contracts/test/UnlockProtocolToken/governor.js +++ b/smart-contracts/test/UnlockProtocolToken/governor.js @@ -1,4 +1,4 @@ -const { ethers, upgrades, network } = require('hardhat') +const { ethers, upgrades } = require('hardhat') const { assert } = require('chai') const { ADDRESS_ZERO, @@ -21,7 +21,6 @@ const defaultQuorum = BigInt('30000') * BigInt(10 ** 18) describe('UP Governor & Timelock', () => { let gov let udt - let updateTx // helper to recreate voting process const launchVotingProcess = async (voter, proposal) => { @@ -34,9 +33,9 @@ describe('UP Governor & Timelock', () => { // proposale exists but does not accep votes yet assert.equal(await gov.state(proposalId), 0) // Pending - // wait for a block (default voting delay) - const currentBlock = await ethers.provider.getBlockNumber() - await advanceBlock(BigInt(currentBlock + 1) + (await gov.votingDelay())) + // wait for voting delay + const timepoint = await gov.proposalSnapshot(proposalId) + await advanceBlockTo(timepoint + 1n) // now ready to receive votes assert.equal(await gov.state(proposalId), 1) // Active @@ -65,7 +64,10 @@ describe('UP Governor & Timelock', () => { const tx = await gov.execute(targets, values, calldatas, descriptionHash) assert.equal(await gov.state(proposalId), 7) // Executed - updateTx = await tx.wait() + const execReceipt = await tx.wait() + const execEvent = await getEvent(execReceipt, 'ProposalExecuted') + assert.notEqual(execEvent, null) // Executed + return execReceipt } before(async () => { @@ -120,13 +122,6 @@ describe('UP Governor & Timelock', () => { const quorum = ethers.parseUnits('15000.0', 18) const [owner, minter, voter] = await ethers.getSigners() - // bring default voting period to 10 blocks for testing purposes - await network.provider.send('hardhat_setStorageAt', [ - await gov.getAddress(), - '0x1c7', // '455' storage slot - '0x0000000000000000000000000000000000000000000000000000000000000032', // 50 blocks - ]) - // get tokens udt = await udt.connect(minter) await udt.mint(await owner.getAddress(), quorum) @@ -156,12 +151,12 @@ describe('UP Governor & Timelock', () => { // propose const proposal = [ [await gov.getAddress()], - [ethers.parseUnits('0')], + ['0'], [encoded], '', ] - await launchVotingProcess(voter, proposal) + const execReceipt = await launchVotingProcess(voter, proposal) const lastBlock = await getLatestBlock() await advanceBlock() @@ -171,7 +166,7 @@ describe('UP Governor & Timelock', () => { assert.equal(changed == quorum, true) // make sure event has been fired - const { args } = await getEvent(updateTx, 'QuorumUpdated') + const { args } = await getEvent(execReceipt, 'QuorumSet') const { oldQuorum, newQuorum } = args assert.equal(newQuorum == quorum, true) assert.equal(oldQuorum, defaultQuorum) @@ -190,28 +185,26 @@ describe('UP Governor & Timelock', () => { // propose const proposal = [ [await gov.getAddress()], - [ethers.parseUnits('0')], + ['0'], [encoded], '', ] - await launchVotingProcess(voter, proposal) + const execTx = await launchVotingProcess(voter, proposal) const changed = await gov.votingPeriod() assert.equal(changed == votingPeriod, true) // make sure event has been fired - const { args } = await getEvent(updateTx, 'VotingPeriodUpdated') - const { oldVotingPeriod, newVotingPeriod } = args + const { args } = await getEvent(execTx, 'VotingPeriodSet') + const { newVotingPeriod } = args assert.equal(newVotingPeriod == votingPeriod, true) - // nb: old value is the one we enforced through eth_storageAt - assert.equal(oldVotingPeriod, 50) }) }) describe('VotingDelay', () => { it('should be properly updated through voting', async () => { - const votingDelay = 10000 + const votingDelay = 10000n const [, , voter] = await ethers.getSigners() const encoded = gov.interface.encodeFunctionData('setVotingDelay', [ @@ -220,21 +213,20 @@ describe('UP Governor & Timelock', () => { const proposal = [ [await gov.getAddress()], - [ethers.parseUnits('0')], + ['0'], [encoded], '', ] - await launchVotingProcess(voter, proposal) + const execTx = await launchVotingProcess(voter, proposal) const changed = await gov.votingDelay() assert.equal(changed == votingDelay, true) // make sure event has been fired - const { args } = await getEvent(updateTx, 'VotingDelayUpdated') - const { oldVotingDelay, newVotingDelay } = args + const { args } = await getEvent(execTx, 'VotingDelaySet') + const { newVotingDelay } = args assert.equal(newVotingDelay, votingDelay) - assert.equal(oldVotingDelay, 1) }) }) }) From 13819ff1fbd742f514f011bb16572740a3002ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 22 May 2024 14:18:50 +0200 Subject: [PATCH 09/18] fix gov quorum event --- smart-contracts/contracts/tokens/UP/UPGovernor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smart-contracts/contracts/tokens/UP/UPGovernor.sol b/smart-contracts/contracts/tokens/UP/UPGovernor.sol index 8472cf44c19..99b9bd8e9bd 100644 --- a/smart-contracts/contracts/tokens/UP/UPGovernor.sol +++ b/smart-contracts/contracts/tokens/UP/UPGovernor.sol @@ -21,7 +21,7 @@ contract UPGovernor is uint private _quorum; // add custom event for quorum changes - event QuorumSet(uint oldVotingDelay, uint newVotingDelay); + event QuorumSet(uint oldQuorum, uint newQuorum); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { From 2a42e60b01ed7dc05e6bb29d63a805f15d60d4a4 Mon Sep 17 00:00:00 2001 From: Julien Genestoux Date: Wed, 22 May 2024 09:32:48 -0400 Subject: [PATCH 10/18] Update smart-contracts/test/UnlockProtocolToken/governor.js --- smart-contracts/test/UnlockProtocolToken/governor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smart-contracts/test/UnlockProtocolToken/governor.js b/smart-contracts/test/UnlockProtocolToken/governor.js index 41a7fb49eb2..b0c569efbf1 100644 --- a/smart-contracts/test/UnlockProtocolToken/governor.js +++ b/smart-contracts/test/UnlockProtocolToken/governor.js @@ -16,7 +16,7 @@ const PROPOSER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('PROPOSER_ROLE')) const SIX_DAYS = 43200 // in blocks const votingDelay = SIX_DAYS // const votingPeriod = SIX_DAYS -const defaultQuorum = BigInt('30000') * BigInt(10 ** 18) +const defaultQuorum = BigInt('3000') * BigInt(10 ** 18) describe('UP Governor & Timelock', () => { let gov From 4465c19bd6c15e1a80886bfa9623480c3691a574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 22 May 2024 16:15:46 +0200 Subject: [PATCH 11/18] Update smart-contracts/contracts/tokens/UP/UPGovernor.sol Co-authored-by: Julien Genestoux --- smart-contracts/contracts/tokens/UP/UPGovernor.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/smart-contracts/contracts/tokens/UP/UPGovernor.sol b/smart-contracts/contracts/tokens/UP/UPGovernor.sol index 99b9bd8e9bd..f1496ad3e17 100644 --- a/smart-contracts/contracts/tokens/UP/UPGovernor.sol +++ b/smart-contracts/contracts/tokens/UP/UPGovernor.sol @@ -42,7 +42,6 @@ contract UPGovernor is _quorum = 30000e18; } - // quorum set to 30k function quorum(uint256) public view override returns (uint256) { return _quorum; } From 24b6e4c031141e6f2bef9da83bbda1b0a8a5456d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 22 May 2024 16:32:08 +0200 Subject: [PATCH 12/18] use seconds instead of blocks --- .../contracts/tokens/UP/UPGovernor.sol | 6 +- .../tokens/UP/UnlockProtocolToken.sol | 10 ++ .../test/UnlockProtocolToken/governor.js | 6 +- .../test/UnlockProtocolToken/votes.js | 164 ++++++++---------- 4 files changed, 84 insertions(+), 102 deletions(-) diff --git a/smart-contracts/contracts/tokens/UP/UPGovernor.sol b/smart-contracts/contracts/tokens/UP/UPGovernor.sol index 99b9bd8e9bd..38d07fc1825 100644 --- a/smart-contracts/contracts/tokens/UP/UPGovernor.sol +++ b/smart-contracts/contracts/tokens/UP/UPGovernor.sol @@ -33,13 +33,13 @@ contract UPGovernor is TimelockControllerUpgradeable _timelock ) public initializer { __Governor_init("UnlockProtocolGovernor"); - __GovernorSettings_init(43200 /* 6 day */, 43200 /* 6 days */, 0); + __GovernorSettings_init(6 days, 6 days, 0); __GovernorCountingSimple_init(); __GovernorVotes_init(_token); __GovernorTimelockControl_init(_timelock); - // default quorum set to 30k - _quorum = 30000e18; + // default quorum set to 3000 + _quorum = 3000e18; } // quorum set to 30k diff --git a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol b/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol index 3ae4cd542ef..15fb1429ddf 100644 --- a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol +++ b/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol @@ -37,6 +37,16 @@ contract UnlockProtocolToken is _mint(preMinter, TOTAL_SUPPLY * 10 ** decimals()); } + // required to base votes on timestamp instead of blocks + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public pure override returns (string memory) { + return "mode=timestamp"; + } + // The following functions are overrides required by Solidity. function _update( diff --git a/smart-contracts/test/UnlockProtocolToken/governor.js b/smart-contracts/test/UnlockProtocolToken/governor.js index 41a7fb49eb2..fa98e15d5ca 100644 --- a/smart-contracts/test/UnlockProtocolToken/governor.js +++ b/smart-contracts/test/UnlockProtocolToken/governor.js @@ -13,10 +13,10 @@ const { getEvent } = require('@unlock-protocol/hardhat-helpers') const PROPOSER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('PROPOSER_ROLE')) // default values -const SIX_DAYS = 43200 // in blocks -const votingDelay = SIX_DAYS // +const SIX_DAYS = 6 * 24 * 60 * 60 // in seconds +const votingDelay = SIX_DAYS const votingPeriod = SIX_DAYS -const defaultQuorum = BigInt('30000') * BigInt(10 ** 18) +const defaultQuorum = BigInt('3000') * BigInt(10 ** 18) describe('UP Governor & Timelock', () => { let gov diff --git a/smart-contracts/test/UnlockProtocolToken/votes.js b/smart-contracts/test/UnlockProtocolToken/votes.js index a7f7770ca10..052e0aa78c4 100644 --- a/smart-contracts/test/UnlockProtocolToken/votes.js +++ b/smart-contracts/test/UnlockProtocolToken/votes.js @@ -9,7 +9,6 @@ const { notExpectEvent, compareBigNumbers, compareBigNumberArrays, - getLatestBlock, } = require('../helpers') const supply = BigInt('100000000') @@ -54,13 +53,6 @@ describe('UnlockProtocolToken / Votes', () => { assert(supply == (await up.balanceOf(holder))) }) }) - // it('minting restriction', async () => { - // const amount = BigInt('2') ** BigInt('96') - // await reverts( - // up.mint(minter, amount), - // 'ERC20Votes: total supply risks overflowing votes' - // ) - // }) }) describe('Delegation', () => { @@ -69,9 +61,8 @@ describe('UnlockProtocolToken / Votes', () => { assert.equal(await up.delegates(minter), ADDRESS_ZERO) const tx = await up.connect(holderSigner).delegate(holder) const receipt = await tx.wait() - const { blockNumber } = receipt + const { timestamp } = await ethers.provider.getBlock(receipt.blockNumber) - // console.log(events) expectEvent(receipt, 'DelegateChanged', { delegator: holder, fromDelegate: ADDRESS_ZERO, @@ -84,9 +75,9 @@ describe('UnlockProtocolToken / Votes', () => { }) compareBigNumbers(supply, await up.getVotes(holder)) - compareBigNumbers('0', await up.getPastVotes(holder, blockNumber - 1)) + compareBigNumbers('0', await up.getPastVotes(holder, timestamp - 1)) await advanceBlock() - compareBigNumbers(supply, await up.getPastVotes(holder, blockNumber)) + compareBigNumbers(supply, await up.getPastVotes(holder, timestamp)) }) it('delegation without balance', async () => { assert.equal(await up.delegates(holder), ADDRESS_ZERO) @@ -115,7 +106,9 @@ describe('UnlockProtocolToken / Votes', () => { const tx = await up.connect(holderSigner).delegate(holderDelegatee) const receipt = await tx.wait() - const { blockNumber } = receipt + const { timestamp } = await ethers.provider.getBlock( + receipt.blockNumber + ) expectEvent(receipt, 'DelegateChanged', { delegator: holder, @@ -137,22 +130,19 @@ describe('UnlockProtocolToken / Votes', () => { compareBigNumbers('0', await up.getVotes(holder)) compareBigNumbers(supply, await up.getVotes(holderDelegatee)) - compareBigNumbers( - supply, - await up.getPastVotes(holder, blockNumber - 1) - ) + compareBigNumbers(supply, await up.getPastVotes(holder, timestamp - 1)) compareBigNumbers( '0', - await up.getPastVotes(holderDelegatee, blockNumber - 1) + await up.getPastVotes(holderDelegatee, timestamp - 1) ) await advanceBlock() - compareBigNumbers('0', await up.getPastVotes(holder, blockNumber)) + compareBigNumbers('0', await up.getPastVotes(holder, timestamp)) compareBigNumbers( supply, - await up.getPastVotes(holderDelegatee, blockNumber) + await up.getPastVotes(holderDelegatee, timestamp) ) }) }) @@ -250,12 +240,12 @@ describe('UnlockProtocolToken / Votes', () => { compareBigNumbers(recipientVotes, await up.getVotes(recipient)) // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const blockNumber = await getLatestBlock() + const { timestamp } = await ethers.provider.getBlock() await advanceBlock() - compareBigNumbers(holderVotes, await up.getPastVotes(holder, blockNumber)) + compareBigNumbers(holderVotes, await up.getPastVotes(holder, timestamp)) compareBigNumbers( recipientVotes, - await up.getPastVotes(recipient, blockNumber) + await up.getPastVotes(recipient, timestamp) ) }) }) @@ -276,43 +266,40 @@ describe('UnlockProtocolToken / Votes', () => { await up.connect(holderSigner).transfer(recipient, '100') // give an account a few tokens for readability compareBigNumbers('0', await up.numCheckpoints(other1)) - const t1 = await up.connect(recipientSigner).delegate(other1) + const tx1 = await up.connect(recipientSigner).delegate(other1) + const { timestamp: t1 } = await ethers.provider.getBlock( + tx1.blockNumber + ) compareBigNumbers('1', await up.numCheckpoints(other1)) - const t2 = await up.connect(recipientSigner).transfer(other2, 10) + const tx2 = await up.connect(recipientSigner).transfer(other2, 10) + const { timestamp: t2 } = await ethers.provider.getBlock( + tx2.blockNumber + ) compareBigNumbers('2', await up.numCheckpoints(other1)) - const t3 = await up.connect(recipientSigner).transfer(other2, 10) + const tx3 = await up.connect(recipientSigner).transfer(other2, 10) + const { timestamp: t3 } = await ethers.provider.getBlock( + tx3.blockNumber + ) compareBigNumbers('3', await up.numCheckpoints(other1)) - const t4 = await up.connect(holderSigner).transfer(recipient, 20) + const tx4 = await up.connect(holderSigner).transfer(recipient, 20) + const { timestamp: t4 } = await ethers.provider.getBlock( + tx4.blockNumber + ) compareBigNumbers('4', await up.numCheckpoints(other1)) - compareBigNumberArrays(await up.checkpoints(other1, 0), [ - t1.blockNumber, - '100', - ]) - compareBigNumberArrays(await up.checkpoints(other1, 1), [ - t2.blockNumber, - '90', - ]) - compareBigNumberArrays(await up.checkpoints(other1, 2), [ - t3.blockNumber, - '80', - ]) - compareBigNumberArrays(await up.checkpoints(other1, 3), [ - t4.blockNumber, - '100', - ]) + compareBigNumberArrays(await up.checkpoints(other1, 0), [t1, '100']) + compareBigNumberArrays(await up.checkpoints(other1, 1), [t2, '90']) + compareBigNumberArrays(await up.checkpoints(other1, 2), [t3, '80']) + compareBigNumberArrays(await up.checkpoints(other1, 3), [t4, '100']) await advanceBlock() - compareBigNumbers('100', await up.getPastVotes(other1, t1.blockNumber)) - - compareBigNumbers('90', await up.getPastVotes(other1, t2.blockNumber)) - - compareBigNumbers('80', await up.getPastVotes(other1, t3.blockNumber)) - - compareBigNumbers('100', await up.getPastVotes(other1, t4.blockNumber)) + compareBigNumbers('100', await up.getPastVotes(other1, t1)) + compareBigNumbers('90', await up.getPastVotes(other1, t2)) + compareBigNumbers('80', await up.getPastVotes(other1, t3)) + compareBigNumbers('100', await up.getPastVotes(other1, t4)) }) }) @@ -328,78 +315,63 @@ describe('UnlockProtocolToken / Votes', () => { it('returns the latest block if >= last checkpoint block', async () => { const t1 = await up.connect(holderSigner).delegate(other1) const { blockNumber } = await t1.wait() + const { timestamp } = await ethers.provider.getBlock(blockNumber) await advanceBlock() await advanceBlock() - compareBigNumbers(supply, await up.getPastVotes(other1, blockNumber)) - - compareBigNumbers( - supply, - await up.getPastVotes(other1, blockNumber + 1) - ) + compareBigNumbers(supply, await up.getPastVotes(other1, timestamp)) + compareBigNumbers(supply, await up.getPastVotes(other1, timestamp + 1)) }) it('returns zero if < first checkpoint block', async () => { await advanceBlock() const t1 = await up.connect(holderSigner).delegate(other1) const { blockNumber } = await t1.wait() + const { timestamp } = await ethers.provider.getBlock(blockNumber) await advanceBlock() await advanceBlock() - compareBigNumbers('0', await up.getPastVotes(other1, blockNumber - 1)) - - compareBigNumbers( - supply, - await up.getPastVotes(other1, blockNumber + 1) - ) + compareBigNumbers('0', await up.getPastVotes(other1, timestamp - 1)) + compareBigNumbers(supply, await up.getPastVotes(other1, timestamp + 1)) }) it('generally returns the voting balance at the appropriate checkpoint', async () => { - const t1 = await up.connect(holderSigner).delegate(other1) + const tx1 = await up.connect(holderSigner).delegate(other1) + const { timestamp: t1 } = await ethers.provider.getBlock( + tx1.blockNumber + ) await advanceBlock() await advanceBlock() - const t2 = await up.connect(holderSigner).transfer(other2, 10) + const tx2 = await up.connect(holderSigner).transfer(other2, 10) + const { timestamp: t2 } = await ethers.provider.getBlock( + tx2.blockNumber + ) await advanceBlock() await advanceBlock() - const t3 = await up.connect(holderSigner).transfer(other2, 10) + const tx3 = await up.connect(holderSigner).transfer(other2, 10) + const { timestamp: t3 } = await ethers.provider.getBlock( + tx3.blockNumber + ) await advanceBlock() await advanceBlock() - const t4 = await up + const tx4 = await up .connect(await ethers.getSigner(other2)) .transfer(holder, 20) + const { timestamp: t4 } = await ethers.provider.getBlock( + tx4.blockNumber + ) await advanceBlock() await advanceBlock() - compareBigNumbers( - '0', - await up.getPastVotes(other1, t1.blockNumber - 1) - ) - compareBigNumbers(supply, await up.getPastVotes(other1, t1.blockNumber)) - compareBigNumbers( - supply, - await up.getPastVotes(other1, t1.blockNumber + 1) - ) - compareBigNumbers( - '99999990', - await up.getPastVotes(other1, t2.blockNumber) - ) - compareBigNumbers( - '99999990', - await up.getPastVotes(other1, t2.blockNumber + 1) - ) - compareBigNumbers( - '99999980', - await up.getPastVotes(other1, t3.blockNumber) - ) - compareBigNumbers( - '99999980', - await up.getPastVotes(other1, t3.blockNumber + 1) - ) - compareBigNumbers(supply, await up.getPastVotes(other1, t4.blockNumber)) - compareBigNumbers( - supply, - await up.getPastVotes(other1, t4.blockNumber + 1) - ) + compareBigNumbers('0', await up.getPastVotes(other1, t1 - 1)) + compareBigNumbers(supply, await up.getPastVotes(other1, t1)) + compareBigNumbers(supply, await up.getPastVotes(other1, t1 + 1)) + compareBigNumbers('99999990', await up.getPastVotes(other1, t2)) + compareBigNumbers('99999990', await up.getPastVotes(other1, t2 + 1)) + compareBigNumbers('99999980', await up.getPastVotes(other1, t3)) + compareBigNumbers('99999980', await up.getPastVotes(other1, t3 + 1)) + compareBigNumbers(supply, await up.getPastVotes(other1, t4)) + compareBigNumbers(supply, await up.getPastVotes(other1, t4 + 1)) }) }) }) From d1621d31be296c232b32d4453fa8b013ac16e9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Wed, 22 May 2024 16:38:20 +0200 Subject: [PATCH 13/18] specifiy timelock admin in constructor --- smart-contracts/contracts/tokens/UP/UPTimelock.sol | 5 +++-- smart-contracts/test/UnlockProtocolToken/governor.js | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/smart-contracts/contracts/tokens/UP/UPTimelock.sol b/smart-contracts/contracts/tokens/UP/UPTimelock.sol index 0278a6e4838..721d3ad0ac2 100644 --- a/smart-contracts/contracts/tokens/UP/UPTimelock.sol +++ b/smart-contracts/contracts/tokens/UP/UPTimelock.sol @@ -7,8 +7,9 @@ contract UPTimelock is TimelockControllerUpgradeable { function initialize( uint256 minDelay, address[] memory proposers, - address[] memory executors + address[] memory executors, + address admin ) public initializer { - __TimelockController_init(minDelay, proposers, executors, msg.sender); + __TimelockController_init(minDelay, proposers, executors, admin); } } diff --git a/smart-contracts/test/UnlockProtocolToken/governor.js b/smart-contracts/test/UnlockProtocolToken/governor.js index fa98e15d5ca..f90ec8b2bf5 100644 --- a/smart-contracts/test/UnlockProtocolToken/governor.js +++ b/smart-contracts/test/UnlockProtocolToken/governor.js @@ -75,6 +75,7 @@ describe('UP Governor & Timelock', () => { }) beforeEach(async () => { + const [timelockAdmin] = await ethers.getSigners() // deploying timelock with a proxy const UPTimelock = await ethers.getContractFactory('UPTimelock') @@ -82,6 +83,7 @@ describe('UP Governor & Timelock', () => { 1, // 1 second delay [], // proposers list is empty at deployment [ADDRESS_ZERO], // allow any address to execute a proposal once the timelock has expired + await timelockAdmin.getAddress(), ]) // deploy governor From 796699c7e4fbc8622433eb329e4a647ddcfb71b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Thu, 23 May 2024 14:56:06 +0200 Subject: [PATCH 14/18] use block timetsmp for revert test --- smart-contracts/test/UnlockProtocolToken/votes.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/smart-contracts/test/UnlockProtocolToken/votes.js b/smart-contracts/test/UnlockProtocolToken/votes.js index 052e0aa78c4..4e274686b96 100644 --- a/smart-contracts/test/UnlockProtocolToken/votes.js +++ b/smart-contracts/test/UnlockProtocolToken/votes.js @@ -304,8 +304,12 @@ describe('UnlockProtocolToken / Votes', () => { }) describe('getPastVotes', () => { - it('reverts if block number >= current block', async () => { - await reverts(up.getPastVotes(other1, 5e10), 'ERC5805FutureLookup') + it('reverts if timestamp >= current time', async () => { + const { timestamp } = await ethers.provider.getBlock() + await reverts( + up.getPastVotes(other1, timestamp + 1), + 'ERC5805FutureLookup' + ) }) it('returns 0 if there are no checkpoints', async () => { From cd71637557e6ac1c9e09acbb05517682f295cf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Thu, 23 May 2024 17:31:58 +0200 Subject: [PATCH 15/18] fix governor testst --- .../contracts/tokens/UP/UPGovernor.sol | 24 +-- .../test/UnlockProtocolToken/governor.js | 166 +++++++++++------- .../test/UnlockProtocolToken/votes.js | 4 +- smart-contracts/test/helpers/time.js | 11 +- 4 files changed, 120 insertions(+), 85 deletions(-) diff --git a/smart-contracts/contracts/tokens/UP/UPGovernor.sol b/smart-contracts/contracts/tokens/UP/UPGovernor.sol index 5a5193ee930..517b70a147a 100644 --- a/smart-contracts/contracts/tokens/UP/UPGovernor.sol +++ b/smart-contracts/contracts/tokens/UP/UPGovernor.sol @@ -2,12 +2,13 @@ // Compatible with OpenZeppelin Contracts ^5.0.0 pragma solidity ^0.8.21; +import "@openzeppelin/contracts-upgradeable5/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable5/governance/GovernorUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorSettingsUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorCountingSimpleUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorVotesUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorTimelockControlUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable5/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable5/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol"; /// @custom:security-contact hello@unlock-protocol.com contract UPGovernor is @@ -16,13 +17,9 @@ contract UPGovernor is GovernorSettingsUpgradeable, GovernorCountingSimpleUpgradeable, GovernorVotesUpgradeable, + GovernorVotesQuorumFractionUpgradeable, GovernorTimelockControlUpgradeable { - uint private _quorum; - - // add custom event for quorum changes - event QuorumSet(uint oldQuorum, uint newQuorum); - /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -36,21 +33,12 @@ contract UPGovernor is __GovernorSettings_init(6 days, 6 days, 0); __GovernorCountingSimple_init(); __GovernorVotes_init(_token); + __GovernorVotesQuorumFraction_init(3); __GovernorTimelockControl_init(_timelock); - - // default quorum set to 3000 - _quorum = 3000e18; - } - - function quorum(uint256) public view override returns (uint256) { - return _quorum; } - // helper to change quorum - function setQuorum(uint256 newQuorum) public onlyGovernance { - uint256 oldQuorum = _quorum; - _quorum = newQuorum; - emit QuorumSet(oldQuorum, newQuorum); + function quorumDenominator() public pure override returns (uint256) { + return 1000; } // The following functions are overrides required by Solidity. diff --git a/smart-contracts/test/UnlockProtocolToken/governor.js b/smart-contracts/test/UnlockProtocolToken/governor.js index f90ec8b2bf5..1ad73704953 100644 --- a/smart-contracts/test/UnlockProtocolToken/governor.js +++ b/smart-contracts/test/UnlockProtocolToken/governor.js @@ -2,12 +2,10 @@ const { ethers, upgrades } = require('hardhat') const { assert } = require('chai') const { ADDRESS_ZERO, - getLatestBlock, - advanceBlock, - advanceBlockTo, + increaseTime, + increaseTimeTo, reverts, } = require('../helpers') -const deployContracts = require('../fixtures/deploy') const { getEvent } = require('@unlock-protocol/hardhat-helpers') const PROPOSER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('PROPOSER_ROLE')) @@ -16,11 +14,14 @@ const PROPOSER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('PROPOSER_ROLE')) const SIX_DAYS = 6 * 24 * 60 * 60 // in seconds const votingDelay = SIX_DAYS const votingPeriod = SIX_DAYS -const defaultQuorum = BigInt('3000') * BigInt(10 ** 18) +const defaultQuorumNumerator = 3n +const defaultQuorumDenominator = 1000n describe('UP Governor & Timelock', () => { let gov - let udt + let up + let admin, minter, delegater, voter, resetter + let expectedQuorum // helper to recreate voting process const launchVotingProcess = async (voter, proposal) => { @@ -35,7 +36,7 @@ describe('UP Governor & Timelock', () => { // wait for voting delay const timepoint = await gov.proposalSnapshot(proposalId) - await advanceBlockTo(timepoint + 1n) + await increaseTimeTo(timepoint + 1n) // now ready to receive votes assert.equal(await gov.state(proposalId), 1) // Active @@ -43,10 +44,13 @@ describe('UP Governor & Timelock', () => { // vote gov = gov.connect(voter) await gov.castVote(proposalId, 1) + await increaseTime() + // const { timestamp } = await ethers.provider.getBlock() + // console.log(await gov.proposalVotes(proposalId, timestamp)) // wait until voting delay is over const deadline = await gov.proposalDeadline(proposalId) - await advanceBlockTo(deadline + 1n) + await increaseTimeTo(deadline + 1n) assert.equal(await gov.state(proposalId), 4) // Succeeded @@ -67,35 +71,45 @@ describe('UP Governor & Timelock', () => { const execReceipt = await tx.wait() const execEvent = await getEvent(execReceipt, 'ProposalExecuted') assert.notEqual(execEvent, null) // Executed + await increaseTime() return execReceipt } before(async () => { - ;({ udt } = await deployContracts()) - }) + ;[admin, minter, delegater, voter, resetter] = await ethers.getSigners() + + // + const UP = await ethers.getContractFactory('UnlockProtocolToken') + up = await upgrades.deployProxy(UP, [ + await admin.getAddress(), + await minter.getAddress(), + ]) - beforeEach(async () => { - const [timelockAdmin] = await ethers.getSigners() // deploying timelock with a proxy const UPTimelock = await ethers.getContractFactory('UPTimelock') - const timelock = await upgrades.deployProxy(UPTimelock, [ 1, // 1 second delay [], // proposers list is empty at deployment [ADDRESS_ZERO], // allow any address to execute a proposal once the timelock has expired - await timelockAdmin.getAddress(), + await admin.getAddress(), ]) // deploy governor const UPGovernor = await ethers.getContractFactory('UPGovernor') - gov = await upgrades.deployProxy(UPGovernor, [ - await udt.getAddress(), + await up.getAddress(), await timelock.getAddress(), ]) // grant role await timelock.grantRole(PROPOSER_ROLE, await gov.getAddress()) + + // increase time so the quorum is taken into account + await increaseTime() + + expectedQuorum = + ((await up.TOTAL_SUPPLY()) * 10n ** 18n * defaultQuorumNumerator) / + defaultQuorumDenominator }) describe('Default values', () => { @@ -107,48 +121,52 @@ describe('UP Governor & Timelock', () => { assert.equal(await gov.votingPeriod(), votingPeriod) }) - it('quorum is 30k UDT', async () => { - assert.equal(await gov.quorum(1), defaultQuorum) + it('default quorum is 0.3% of total supply', async () => { + const { timestamp } = await ethers.provider.getBlock() + assert.equal(await gov.quorumNumerator(), defaultQuorumNumerator) + assert.equal(await gov.quorumDenominator(), defaultQuorumDenominator) + assert.equal( + await gov['quorumNumerator(uint256)'](timestamp), + defaultQuorumNumerator + ) + assert.equal(await gov.quorum(timestamp - 1), expectedQuorum) }) }) describe('Update voting params', () => { - it('should only be possible through voting', async () => { - assert.equal(await gov.votingDelay(), votingDelay) - await reverts(gov.setVotingDelay(2), 'GovernorOnlyExecutor') - await reverts(gov.setQuorum(2), 'GovernorOnlyExecutor') - await reverts(gov.setVotingPeriod(2), 'GovernorOnlyExecutor') - }) + before(async () => { + // transfer and delegate anout tokens for voter to reach quorum alone + await up + .connect(minter) + .transfer(await delegater.getAddress(), expectedQuorum + 1n) + await up.connect(delegater).delegate(await voter.getAddress()) - beforeEach(async () => { - const quorum = ethers.parseUnits('15000.0', 18) - const [owner, minter, voter] = await ethers.getSigners() - - // get tokens - udt = await udt.connect(minter) - await udt.mint(await owner.getAddress(), quorum) - - // give voter a few more tokens of its own to make sure we are above quorum - await udt.mint(await minter.getAddress(), ethers.parseUnits('10.0', 18)) - await udt.delegate(await voter.getAddress()) - - // delegate votes - udt = await udt.connect(owner) - const tx = await udt.delegate(await voter.getAddress()) - await tx.wait() + const { timestamp } = await ethers.provider.getBlock() + await increaseTime() + // make sure voter as enough vote assert.equal( - (await udt.getVotes(await voter.getAddress())) > quorum, + (await up.getVotes(await voter.getAddress())) > + (await gov.quorum(timestamp)), true ) }) + it('should only be possible through voting', async () => { + assert.equal(await gov.votingDelay(), votingDelay) + await reverts(gov.setVotingDelay(2), 'GovernorOnlyExecutor') + await reverts(gov.updateQuorumNumerator(2), 'GovernorOnlyExecutor') + await reverts(gov.setVotingPeriod(2), 'GovernorOnlyExecutor') + }) + describe('Quorum', () => { it('should be properly updated through voting', async () => { - const quorum = ethers.parseUnits('35.0', 18) + const newQuorumNumerator = 10n - const [, , voter] = await ethers.getSigners() - const encoded = gov.interface.encodeFunctionData('setQuorum', [quorum]) + const encoded = gov.interface.encodeFunctionData( + 'updateQuorumNumerator', + [newQuorumNumerator] + ) // propose const proposal = [ @@ -160,18 +178,23 @@ describe('UP Governor & Timelock', () => { const execReceipt = await launchVotingProcess(voter, proposal) - const lastBlock = await getLatestBlock() - await advanceBlock() - // make sure quorum has been changed succesfully - const changed = await gov.quorum(lastBlock) - assert.equal(changed == quorum, true) + const { timestamp } = await ethers.provider.getBlock( + execReceipt.blockNumber + ) + + assert.equal(await gov.quorumNumerator(), newQuorumNumerator) + assert.equal( + await gov.quorum(timestamp), + ((await up.TOTAL_SUPPLY()) * 10n ** 18n * newQuorumNumerator) / + defaultQuorumDenominator + ) // make sure event has been fired - const { args } = await getEvent(execReceipt, 'QuorumSet') - const { oldQuorum, newQuorum } = args - assert.equal(newQuorum == quorum, true) - assert.equal(oldQuorum, defaultQuorum) + const { args } = await getEvent(execReceipt, 'QuorumNumeratorUpdated') + + assert.equal(args.newQuorumNumerator, newQuorumNumerator) + assert.equal(args.oldQuorumNumerator, defaultQuorumNumerator) }) }) @@ -179,7 +202,6 @@ describe('UP Governor & Timelock', () => { it('should be properly updated through voting', async () => { const votingPeriod = 10 - const [, , voter] = await ethers.getSigners() const encoded = gov.interface.encodeFunctionData('setVotingPeriod', [ votingPeriod, ]) @@ -207,8 +229,6 @@ describe('UP Governor & Timelock', () => { describe('VotingDelay', () => { it('should be properly updated through voting', async () => { const votingDelay = 10000n - - const [, , voter] = await ethers.getSigners() const encoded = gov.interface.encodeFunctionData('setVotingDelay', [ votingDelay, ]) @@ -220,16 +240,44 @@ describe('UP Governor & Timelock', () => { '', ] - const execTx = await launchVotingProcess(voter, proposal) - + const execReceipt = await launchVotingProcess(voter, proposal) const changed = await gov.votingDelay() assert.equal(changed == votingDelay, true) // make sure event has been fired - const { args } = await getEvent(execTx, 'VotingDelaySet') + const { args } = await getEvent(execReceipt, 'VotingDelaySet') const { newVotingDelay } = args assert.equal(newVotingDelay, votingDelay) }) }) + + afterEach(async () => { + // reset to original state after tests + const { timestamp } = await ethers.provider.getBlock() + await increaseTime() + const quorum = await gov.quorum(timestamp) + + // transfer and delegate anout tokens for voter to reach quorum alone + if (quorum > expectedQuorum) { + await up + .connect(minter) + .transfer(await resetter.getAddress(), quorum + 1n) + await up.connect(resetter).delegate(await resetter.getAddress()) + await increaseTime() + + // reset quorum through proposal + const proposal = [ + [await gov.getAddress()], + ['0'], + [ + gov.interface.encodeFunctionData('updateQuorumNumerator', [ + defaultQuorumNumerator, + ]), + ], + ``, + ] + await launchVotingProcess(resetter, proposal) + } + }) }) }) diff --git a/smart-contracts/test/UnlockProtocolToken/votes.js b/smart-contracts/test/UnlockProtocolToken/votes.js index 4e274686b96..b3dac691028 100644 --- a/smart-contracts/test/UnlockProtocolToken/votes.js +++ b/smart-contracts/test/UnlockProtocolToken/votes.js @@ -32,10 +32,10 @@ describe('UnlockProtocolToken / Votes', () => { ;({ address: holder } = holderSigner) ;({ address: recipient } = recipientSigner) - const UnlockDiscountTokenV3 = await ethers.getContractFactory( + const UnlockProtocolToken = await ethers.getContractFactory( 'UnlockProtocolToken' ) - up = await upgrades.deployProxy(UnlockDiscountTokenV3, [ + up = await upgrades.deployProxy(UnlockProtocolToken, [ owner, await minter.getAddress(), ]) diff --git a/smart-contracts/test/helpers/time.js b/smart-contracts/test/helpers/time.js index 3f9d3428401..f415d892698 100644 --- a/smart-contracts/test/helpers/time.js +++ b/smart-contracts/test/helpers/time.js @@ -1,11 +1,10 @@ const helpers = require('@nomicfoundation/hardhat-network-helpers') -const { network, ethers } = require('hardhat') +const { ethers } = require('hardhat') -async function increaseTime(durationInHours) { - const { timestamp } = await ethers.provider.getBlock('latest') - await network.provider.send('evm_increaseTime', [ - (BigInt(durationInHours) * 3600n + BigInt(timestamp)).toString(), - ]) +async function increaseTime(durationInSec = 1) { + const { timestamp } = await ethers.provider.getBlock() + const newTimestamp = timestamp + durationInSec + await increaseTimeTo(newTimestamp) } async function advanceBlock() { From 649f09d91423fead7b97ab25a69f1bf18ebf3526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Thu, 23 May 2024 17:33:16 +0200 Subject: [PATCH 16/18] `increaseTime` receives seconds instead of hours --- smart-contracts/test/UnlockDiscountToken/grantTokens.js | 2 +- smart-contracts/test/UnlockDiscountToken/upgrades.js | 2 +- smart-contracts/test/helpers/uniswapV2.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/smart-contracts/test/UnlockDiscountToken/grantTokens.js b/smart-contracts/test/UnlockDiscountToken/grantTokens.js index fae45f9a6d6..31f2a91455e 100644 --- a/smart-contracts/test/UnlockDiscountToken/grantTokens.js +++ b/smart-contracts/test/UnlockDiscountToken/grantTokens.js @@ -63,7 +63,7 @@ describe('UnlockDiscountToken (l2/sidechain) / granting Tokens', () => { await unlock.setOracle(await udt.getAddress(), await oracle.getAddress()) // Advance time so 1 full period has past and then update again so we have data point to read - await increaseTime(30) + await increaseTime(30 * 3600) await oracle.update(await weth.getAddress(), await udt.getAddress()) // Purchase a valid key for the referrer diff --git a/smart-contracts/test/UnlockDiscountToken/upgrades.js b/smart-contracts/test/UnlockDiscountToken/upgrades.js index a7c103adf69..cec7e93ff45 100644 --- a/smart-contracts/test/UnlockDiscountToken/upgrades.js +++ b/smart-contracts/test/UnlockDiscountToken/upgrades.js @@ -202,7 +202,7 @@ describe('UnlockDiscountToken upgrade', async () => { await unlock.setOracle(await udt.getAddress(), await oracle.getAddress()) // Advance time so 1 full period has past and then update again so we have data point to read - await increaseTime(30) + await increaseTime(30 * 3600) await oracle.update(await weth.getAddress(), await udt.getAddress()) // Purchase a valid key for the referrers diff --git a/smart-contracts/test/helpers/uniswapV2.js b/smart-contracts/test/helpers/uniswapV2.js index 063dc0a04a4..37084dd8892 100644 --- a/smart-contracts/test/helpers/uniswapV2.js +++ b/smart-contracts/test/helpers/uniswapV2.js @@ -85,8 +85,8 @@ const createUniswapV2Exchange = async ({ protocolOwner ) - // Advancing time to avoid an intermittent test fail - await increaseTime(1) + // Advancing one hour to avoid an intermittent test fail + await increaseTime(3600) // Do a swap so there is some data accumulated await uniswapRouter From 4c7195e90e9da6e0ae088e68867dfc61d774168b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Fri, 24 May 2024 12:10:41 +0200 Subject: [PATCH 17/18] rename files to UPToken --- .../tokens/UP/{UnlockProtocolToken.sol => UPToken.sol} | 2 +- .../test/{UnlockProtocolToken => UPToken}/governor.js | 4 ++-- .../test/{UnlockProtocolToken => UPToken}/initialization.js | 4 ++-- .../test/{UnlockProtocolToken => UPToken}/votes.js | 6 ++---- 4 files changed, 7 insertions(+), 9 deletions(-) rename smart-contracts/contracts/tokens/UP/{UnlockProtocolToken.sol => UPToken.sol} (98%) rename smart-contracts/test/{UnlockProtocolToken => UPToken}/governor.js (98%) rename smart-contracts/test/{UnlockProtocolToken => UPToken}/initialization.js (89%) rename smart-contracts/test/{UnlockProtocolToken => UPToken}/votes.js (99%) diff --git a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol b/smart-contracts/contracts/tokens/UP/UPToken.sol similarity index 98% rename from smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol rename to smart-contracts/contracts/tokens/UP/UPToken.sol index 15fb1429ddf..6488cc76f97 100644 --- a/smart-contracts/contracts/tokens/UP/UnlockProtocolToken.sol +++ b/smart-contracts/contracts/tokens/UP/UPToken.sol @@ -10,7 +10,7 @@ import "@openzeppelin/contracts-upgradeable5/proxy/utils/Initializable.sol"; import {NoncesUpgradeable} from "@openzeppelin/contracts-upgradeable5/utils/NoncesUpgradeable.sol"; /// @custom:security-contact hello@unlock-protocol.com -contract UnlockProtocolToken is +contract UPToken is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable, diff --git a/smart-contracts/test/UnlockProtocolToken/governor.js b/smart-contracts/test/UPToken/governor.js similarity index 98% rename from smart-contracts/test/UnlockProtocolToken/governor.js rename to smart-contracts/test/UPToken/governor.js index 1ad73704953..c865b9e2153 100644 --- a/smart-contracts/test/UnlockProtocolToken/governor.js +++ b/smart-contracts/test/UPToken/governor.js @@ -17,7 +17,7 @@ const votingPeriod = SIX_DAYS const defaultQuorumNumerator = 3n const defaultQuorumDenominator = 1000n -describe('UP Governor & Timelock', () => { +describe('UPToken Governor & Timelock', () => { let gov let up let admin, minter, delegater, voter, resetter @@ -79,7 +79,7 @@ describe('UP Governor & Timelock', () => { ;[admin, minter, delegater, voter, resetter] = await ethers.getSigners() // - const UP = await ethers.getContractFactory('UnlockProtocolToken') + const UP = await ethers.getContractFactory('UPToken') up = await upgrades.deployProxy(UP, [ await admin.getAddress(), await minter.getAddress(), diff --git a/smart-contracts/test/UnlockProtocolToken/initialization.js b/smart-contracts/test/UPToken/initialization.js similarity index 89% rename from smart-contracts/test/UnlockProtocolToken/initialization.js rename to smart-contracts/test/UPToken/initialization.js index b6d7f99f5a0..df7bd21006d 100644 --- a/smart-contracts/test/UnlockProtocolToken/initialization.js +++ b/smart-contracts/test/UPToken/initialization.js @@ -1,13 +1,13 @@ const { assert } = require('chai') const { ethers, upgrades } = require('hardhat') -describe('UnlockProtocolToken / initialization', () => { +describe('UPToken / initialization', () => { let owner, preMinter let up before(async () => { ;[owner, preMinter] = await ethers.getSigners() - const UP = await ethers.getContractFactory('UnlockProtocolToken') + const UP = await ethers.getContractFactory('UPToken') up = await upgrades.deployProxy(UP, [ await owner.getAddress(), await preMinter.getAddress(), diff --git a/smart-contracts/test/UnlockProtocolToken/votes.js b/smart-contracts/test/UPToken/votes.js similarity index 99% rename from smart-contracts/test/UnlockProtocolToken/votes.js rename to smart-contracts/test/UPToken/votes.js index b3dac691028..154e7504da9 100644 --- a/smart-contracts/test/UnlockProtocolToken/votes.js +++ b/smart-contracts/test/UPToken/votes.js @@ -13,7 +13,7 @@ const { const supply = BigInt('100000000') -describe('UnlockProtocolToken / Votes', () => { +describe('UPToken / Votes', () => { let up let transferToken let holderSigner, recipientSigner @@ -32,9 +32,7 @@ describe('UnlockProtocolToken / Votes', () => { ;({ address: holder } = holderSigner) ;({ address: recipient } = recipientSigner) - const UnlockProtocolToken = await ethers.getContractFactory( - 'UnlockProtocolToken' - ) + const UnlockProtocolToken = await ethers.getContractFactory('UPToken') up = await upgrades.deployProxy(UnlockProtocolToken, [ owner, await minter.getAddress(), From 68ebc0801e0072b4d233596e4c2862b7cfb20edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Tue, 28 May 2024 11:10:08 +0200 Subject: [PATCH 18/18] Update governor.js Co-authored-by: Julien Genestoux --- smart-contracts/test/UPToken/governor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smart-contracts/test/UPToken/governor.js b/smart-contracts/test/UPToken/governor.js index c865b9e2153..d9b1dfed83c 100644 --- a/smart-contracts/test/UPToken/governor.js +++ b/smart-contracts/test/UPToken/governor.js @@ -31,7 +31,7 @@ describe('UPToken Governor & Timelock', () => { const evt = await getEvent(receipt, 'ProposalCreated') const { proposalId } = evt.args - // proposale exists but does not accep votes yet + // proposal exists but does not accept votes yet assert.equal(await gov.state(proposalId), 0) // Pending // wait for voting delay