From 1ae60b1f9136d1004ffe50f4068038c0f725cc15 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 5 Feb 2025 13:56:57 -1000 Subject: [PATCH 1/7] Wrote a migration vesting vault --- .gitmodules | 3 + foundry.toml | 1 + lib/council | 1 + src/MigrationVestingVault.sol | 106 ++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 160000 lib/council create mode 100644 src/MigrationVestingVault.sol diff --git a/.gitmodules b/.gitmodules index 690924b..20d19a9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/council"] + path = lib/council + url = https://github.com/delvtech/council diff --git a/foundry.toml b/foundry.toml index 4c3f2a9..0954faa 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,5 +4,6 @@ out = "out" libs = ["lib"] remappings = [ + 'council/=lib/council/contracts', 'openzeppelin/=lib/openzeppelin-contracts/contracts', ] diff --git a/lib/council b/lib/council new file mode 160000 index 0000000..5f7be33 --- /dev/null +++ b/lib/council @@ -0,0 +1 @@ +Subproject commit 5f7be330b05f1c3bebd0176882cc5c3429f0764f diff --git a/src/MigrationVestingVault.sol b/src/MigrationVestingVault.sol new file mode 100644 index 0000000..9d2b9a9 --- /dev/null +++ b/src/MigrationVestingVault.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +import { IERC20 } from "council/interfaces/IERC20.sol"; +import { IVotingVault } from "council/interfaces/IVotingVault.sol"; +import { History } from "council/libraries/History.sol"; +import { VestingVaultStorage } from "council/libraries/VestingVaultStorage.sol"; +import { Storage } from "council/libraries/Storage.sol"; +import { AbstractVestingVault } from "council/vaults/VestingVault.sol"; + +// FIXME: Scrutinize this. +// +// FIXME: Test this. +// +/// @title MigrationVestingVault +/// @notice A migration vault that converts ELFI tokens to HD tokens. Migrated +/// tokens are granted with a linear vesting schedule of three months. +/// The grant is created at a destination address provided by the +/// migrator. This contract inherits full voting power tracking from +/// `AbstractVestingVault`. +contract MigrationVestingVault is AbstractVestingVault { + using History for History.HistoricalBalances; + + // The ELFI token to migrate from. + IERC20 public immutable elfiToken; + + // The conversion rate from ELFI to HD. + uint256 public immutable conversionMultiplier; + + // The global expiration block at which all grants fully vest. + uint256 public immutable globalExpiration; + + /// @notice Constructs the migration vault. + /// @param _hdToken The ERC20 token to be vested (HD token). + /// @param _elfiToken The ERC20 token to migrate from (ELFI token). + /// @param _stale The stale block lag used in voting power calculations. + /// @param _conversionMultiplier The conversion multiplier from ELFI to HD. + /// @param _globalExpiration The global expiration block for all grants. + constructor( + IERC20 _hdToken, + IERC20 _elfiToken, + uint256 _stale, + uint256 _conversionMultiplier, + uint256 _globalExpiration + ) AbstractVestingVault(_hdToken, _stale) { + elfiToken = _elfiToken; + conversionMultiplier = _conversionMultiplier; + globalExpiration = _globalExpiration; + } + + /// @notice Migrates a specified amount of ELFI tokens into a vesting grant of HD tokens. + /// @dev The caller must have approved this contract for the ELFI token amount. + /// The destination address must not have an existing grant. + /// @param amount The number of tokens to migrate (in ELFI units). + /// @param destination The address at which the vesting grant will be created. + function migrate(uint256 amount, address destination) external { + // Ensure the destination does not already have an active grant. + VestingVaultStorage.Grant storage existingGrant = _grants()[destination]; + require(existingGrant.allocation == 0, "Destination already has grant"); + + // Transfer ELFI tokens from the caller to this contract. + require( + elfiToken.transferFrom(msg.sender, address(this), amount), + "ELFI transfer failed" + ); + + // Calculate the HD token amount to be granted. + uint256 hdAmount = amount * conversionMultiplier; + + // Ensure sufficient HD tokens are available in the unassigned pool. + Storage.Uint256 storage unassigned = _unassigned(); + require(unassigned.data >= hdAmount, "Insufficient HD tokens available"); + + // Set vesting parameters. + uint128 startBlock = uint128(block.number); + // Use the global expiration for all grants. + uint128 expiration = uint128(globalExpiration); + // Vesting starts immediately. + uint128 cliff = startBlock; + + // Calculate the initial voting power using the current unvested multiplier. + Storage.Uint256 memory unvestedMultiplier = _unvestedMultiplier(); + uint128 initialVotingPower = uint128((hdAmount * uint128(unvestedMultiplier.data)) / 100); + + // Create the grant at the destination address. + _grants()[destination] = VestingVaultStorage.Grant({ + allocation: uint128(hdAmount), + withdrawn: 0, + created: startBlock, + expiration: expiration, + cliff: cliff, + latestVotingPower: initialVotingPower, + delegatee: destination, + range: [uint256(0), uint256(0)] + }); + + // Deduct the granted tokens from the unassigned pool. + unassigned.data -= hdAmount; + + // Update the destination's voting power. + History.HistoricalBalances memory votingPower = History.load("votingPower"); + uint256 currentVotes = votingPower.find(destination, block.number); + votingPower.push(destination, currentVotes + initialVotingPower); + emit VoteChange(destination, destination, int256(uint256(initialVotingPower))); + } +} From e333e15c6f98ebc7d383b703c4924dcc162cba16 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 10 Feb 2025 13:52:59 -1000 Subject: [PATCH 2/7] Use custom errors in the MigrationVestingVault --- src/MigrationVestingVault.sol | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/MigrationVestingVault.sol b/src/MigrationVestingVault.sol index 9d2b9a9..8b65ccb 100644 --- a/src/MigrationVestingVault.sol +++ b/src/MigrationVestingVault.sol @@ -12,6 +12,8 @@ import { AbstractVestingVault } from "council/vaults/VestingVault.sol"; // // FIXME: Test this. // +// FIXME: Set the unvested multiplier to 100 in tests. +// /// @title MigrationVestingVault /// @notice A migration vault that converts ELFI tokens to HD tokens. Migrated /// tokens are granted with a linear vesting schedule of three months. @@ -21,7 +23,16 @@ import { AbstractVestingVault } from "council/vaults/VestingVault.sol"; contract MigrationVestingVault is AbstractVestingVault { using History for History.HistoricalBalances; - // The ELFI token to migrate from. + /// @dev Thrown when an existing grant is found. + error ExistingGrantFound(); + + /// @dev Thrown when ELFI transfers fail. + error ElfiTransferFailed(); + + /// @dev Thrown when there are insufficient HD tokens. + error InsufficientHDTokens(); + + /// @dev The ELFI token to migrate from. IERC20 public immutable elfiToken; // The conversion rate from ELFI to HD. @@ -56,20 +67,19 @@ contract MigrationVestingVault is AbstractVestingVault { function migrate(uint256 amount, address destination) external { // Ensure the destination does not already have an active grant. VestingVaultStorage.Grant storage existingGrant = _grants()[destination]; - require(existingGrant.allocation == 0, "Destination already has grant"); + if (existingGrant.allocation != 0) revert ExistingGrantFound(); // Transfer ELFI tokens from the caller to this contract. - require( - elfiToken.transferFrom(msg.sender, address(this), amount), - "ELFI transfer failed" - ); + if (!elfiToken.transferFrom(msg.sender, address(this), amount)) { + revert ElfiTransferFailed(); + } // Calculate the HD token amount to be granted. uint256 hdAmount = amount * conversionMultiplier; // Ensure sufficient HD tokens are available in the unassigned pool. Storage.Uint256 storage unassigned = _unassigned(); - require(unassigned.data >= hdAmount, "Insufficient HD tokens available"); + if (unassigned.data < hdAmount) revert InsufficientHDTokens(); // Set vesting parameters. uint128 startBlock = uint128(block.number); From 24b10364660446bde589a3f95afba7ade016ccc2 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 10 Feb 2025 14:51:29 -1000 Subject: [PATCH 3/7] Started work on the migration vault tests --- foundry.toml | 3 + test/MigrationVestingVaultTest.t.sol | 258 +++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 test/MigrationVestingVaultTest.t.sol diff --git a/foundry.toml b/foundry.toml index 0954faa..7d2fa25 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,3 +7,6 @@ remappings = [ 'council/=lib/council/contracts', 'openzeppelin/=lib/openzeppelin-contracts/contracts', ] + +[rpc_endpoints] +mainnet = "${MAINNET_RPC_URL}" diff --git a/test/MigrationVestingVaultTest.t.sol b/test/MigrationVestingVaultTest.t.sol new file mode 100644 index 0000000..a478d08 --- /dev/null +++ b/test/MigrationVestingVaultTest.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +import { Test } from "forge-std/Test.sol"; +import { HDToken } from "../src/HDToken.sol"; +import { IERC20 } from "council/interfaces/IERC20.sol"; +import { CoreVoting } from "council/CoreVoting.sol"; +import { MigrationVestingVault } from "../src/MigrationVestingVault.sol"; +import { VestingVaultStorage } from "council/libraries/VestingVaultStorage.sol"; + +/// @dev This test suite provides coverage for the MigrationVestingVault contract's +/// functionality, including migration, voting power tracking, and delegation. +contract MigrationVestingVaultTest is Test { + /// @dev Events to test + event VoteChange(address indexed from, address indexed to, int256 amount); + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @dev Constants for mainnet contracts and configuration + uint256 internal constant FORK_BLOCK = 19_000_000; + uint256 internal constant STALE_BLOCKS = 100; + uint256 internal constant CONVERSION_MULTIPLIER = 1; + uint256 internal constant VESTING_DURATION = 90 days; + address internal constant ELFI_WHALE = 0x6De73946eab234F1EE61256F10067D713aF0e37A; + + /// @dev Contract instances + CoreVoting internal constant CORE_VOTING = CoreVoting(0xEaCD577C3F6c44C3ffA398baaD97aE12CDCFed4a); + IERC20 internal constant ELFI = IERC20(0x5c6D51ecBA4D8E4F20373e3ce96a62342B125D6d); + MigrationVestingVault internal vault; + HDToken internal hdToken; + + /// @dev Test accounts + address internal deployer; + address internal alice; + address internal bob; + address internal charlie; + + /// @notice Sets up the test environment with the following: + /// 1. Fork mainnet at specified block + /// 2. Set up test accounts + /// 3. Deploy HDToken and MigrationVestingVault + /// 4. Configure CoreVoting with the new vault + function setUp() public { + // Create test accounts + deployer = address(this); + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + + // Fork mainnet + vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK); + + // Deploy HD token + vm.startPrank(deployer); + hdToken = new HDToken( + "HD Token", + "HD", + block.timestamp + 1 days + ); + + // Deploy migration vault + console.log("test: 1"); + uint256 globalExpiration = block.number + (VESTING_DURATION / 12); + vault = new MigrationVestingVault( + IERC20(address(hdToken)), // Cast HDToken to IERC20 + ELFI, + STALE_BLOCKS, + CONVERSION_MULTIPLIER, + globalExpiration + ); + // FIXME: This is a bit janky. It would be better to use the real timelock. + vault.initialize(deployer, deployer); + console.log("test: 2"); + + // Add vault to CoreVoting + vm.startPrank(CORE_VOTING.owner()); + CORE_VOTING.changeVaultStatus(address(vault), true); + console.log("test: 3"); + + // FIXME: This is janky. It would be better if the vault pulled directly + // from an address. + // + // Fund vault with HD tokens for migration + vm.startPrank(deployer); + hdToken.approve(address(vault), hdToken.totalSupply()); + vault.deposit(hdToken.totalSupply()); + console.log("test: 4"); + + // Fund the addresses with ELFI. + vm.startPrank(ELFI_WHALE); + uint256 whaleBalance = ELFI.balanceOf(ELFI_WHALE); + address[] memory accounts = new address[](3); + accounts[0] = alice; + accounts[1] = bob; + accounts[2] = charlie; + for (uint256 i = 0; i < accounts.length; i++) { + ELFI.transfer(accounts[i], whaleBalance / accounts.length); + } + } + + // ============================== + // Migration Tests + // ============================== + + /// @dev Ensures migration fails when destination already has a grant + function test_migrate_failure_existingGrant() external { + // First migration + uint256 amount = 100e18; + + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, alice); + + // Attempt second migration to same destination + vm.expectRevert(MigrationVestingVault.ExistingGrantFound.selector); + vault.migrate(amount, alice); + vm.stopPrank(); + } + + /// @dev Ensures migration fails when ELFI transfer fails + function test_migrate_failure_transferFailed() external { + uint256 amount = 100e18; + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + + vm.expectRevert(MigrationVestingVault.ElfiTransferFailed.selector); + vault.migrate(amount, alice); + vm.stopPrank(); + } + + /// @dev Ensures migration fails when insufficient HD tokens are available + function test_migrate_failure_insufficientHDTokens() external { + uint256 excessiveAmount = hdToken.balanceOf(address(vault)) + 1; + + vm.startPrank(alice); + ELFI.approve(address(vault), excessiveAmount); + + vm.expectRevert(MigrationVestingVault.InsufficientHDTokens.selector); + vault.migrate(excessiveAmount, alice); + vm.stopPrank(); + } + + /// @dev Ensures successful migration with correct grant creation + function test_migrate_success() external { + uint256 amount = 100e18; + + // Record initial states + uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); + uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + + // Expect Transfer events + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(vault), amount); + + vault.migrate(amount, alice); + vm.stopPrank(); + + // Verify grant parameters + VestingVaultStorage.Grant memory grant = vault.getGrant(alice); + + assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); + assertEq(grant.withdrawn, 0, "Should not have withdrawals"); + assertEq(grant.cliff, grant.created, "Cliff should equal creation block"); + assertEq(grant.expiration, vault.globalExpiration(), "Wrong expiration"); + assertEq(grant.delegatee, alice, "Wrong delegatee"); + + // Verify token transfers + assertEq( + ELFI.balanceOf(address(vault)), + vaultElfiBalanceBefore + amount, + "Vault ELFI balance not updated" + ); + assertEq( + ELFI.balanceOf(alice), + aliceElfiBalanceBefore - amount, + "Alice ELFI balance not updated" + ); + } + + // ============================== + // Voting Power Tests + // ============================== + + /// @dev Ensures correct voting power calculation after migration + function test_votingPower_afterMigration() external { + uint256 amount = 100e18; + + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, alice); + vm.stopPrank(); + + // Wait for power to be queryable (past stale blocks) + vm.roll(block.number + STALE_BLOCKS + 1); + + // Get voting power of alice at the last block number + uint256 votingPower = vault.queryVotePower(alice, block.number - 1, ""); + assertEq( + votingPower, + amount * CONVERSION_MULTIPLIER, + "Incorrect voting power" + ); + } + + /// @dev Tests voting power transfer through delegation + function test_votingPower_afterDelegation() external { + uint256 amount = 100e18; + + // Set up initial grant + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, alice); + + // Delegate to bob + vm.expectEmit(true, true, true, true); + emit VoteChange(alice, bob, int256(uint256(amount * CONVERSION_MULTIPLIER))); + vault.delegate(bob); + vm.stopPrank(); + + // Wait for power to be queryable + vm.roll(block.number + STALE_BLOCKS + 1); + + // Verify voting powers + uint256 aliceVotingPower = vault.queryVotePower(alice, block.number - 1, ""); + uint256 bobVotingPower = vault.queryVotePower(bob, block.number - 1, ""); + + assertEq(aliceVotingPower, 0, "Alice should have no voting power"); + assertEq( + bobVotingPower, + amount * CONVERSION_MULTIPLIER, + "Bob should have Alice's voting power" + ); + } + + /// @dev Tests voting power changes through vesting progression + function test_votingPower_throughVesting() external { + uint256 amount = 100e18; + + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, alice); + vm.stopPrank(); + + // Move to middle of vesting period + uint256 halfwayBlock = vault.globalExpiration() / 2; + vm.roll(halfwayBlock); + + // Check voting power is maintained through vesting + uint256 votingPower = vault.queryVotePower(alice, block.number - 1, ""); + assertEq( + votingPower, + amount * CONVERSION_MULTIPLIER, + "Voting power should remain constant" + ); + } +} From 0d3c5a11ce09b4dba8216dd446bf461a494c5ac4 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 11 Feb 2025 14:22:38 -1000 Subject: [PATCH 4/7] Fixed remaining tests --- src/MigrationVestingVault.sol | 22 ++++++++++++-------- test/MigrationVestingVaultTest.t.sol | 30 ++++++++++++++-------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/MigrationVestingVault.sol b/src/MigrationVestingVault.sol index 8b65ccb..e01c86c 100644 --- a/src/MigrationVestingVault.sol +++ b/src/MigrationVestingVault.sol @@ -12,7 +12,11 @@ import { AbstractVestingVault } from "council/vaults/VestingVault.sol"; // // FIXME: Test this. // -// FIXME: Set the unvested multiplier to 100 in tests. +// FIXME: We can retrofit this to pull funds from the treasury instead of needing +// to be funded directly by the treasury. This would be more flexible since we'll +// have two migration vaults. +// +// FIXME: What should happen to the ELFI? Should it be burned? // /// @title MigrationVestingVault /// @notice A migration vault that converts ELFI tokens to HD tokens. Migrated @@ -67,7 +71,9 @@ contract MigrationVestingVault is AbstractVestingVault { function migrate(uint256 amount, address destination) external { // Ensure the destination does not already have an active grant. VestingVaultStorage.Grant storage existingGrant = _grants()[destination]; - if (existingGrant.allocation != 0) revert ExistingGrantFound(); + if (existingGrant.allocation != 0) { + revert ExistingGrantFound(); + } // Transfer ELFI tokens from the caller to this contract. if (!elfiToken.transferFrom(msg.sender, address(this), amount)) { @@ -79,13 +85,14 @@ contract MigrationVestingVault is AbstractVestingVault { // Ensure sufficient HD tokens are available in the unassigned pool. Storage.Uint256 storage unassigned = _unassigned(); - if (unassigned.data < hdAmount) revert InsufficientHDTokens(); + if (unassigned.data < hdAmount) { + revert InsufficientHDTokens(); + } - // Set vesting parameters. + // Set the vesting parameters. We use the global expiration for all + // grants, and the vesting starts immediately. uint128 startBlock = uint128(block.number); - // Use the global expiration for all grants. uint128 expiration = uint128(globalExpiration); - // Vesting starts immediately. uint128 cliff = startBlock; // Calculate the initial voting power using the current unvested multiplier. @@ -109,8 +116,7 @@ contract MigrationVestingVault is AbstractVestingVault { // Update the destination's voting power. History.HistoricalBalances memory votingPower = History.load("votingPower"); - uint256 currentVotes = votingPower.find(destination, block.number); - votingPower.push(destination, currentVotes + initialVotingPower); + votingPower.push(destination, initialVotingPower); emit VoteChange(destination, destination, int256(uint256(initialVotingPower))); } } diff --git a/test/MigrationVestingVaultTest.t.sol b/test/MigrationVestingVaultTest.t.sol index a478d08..063e810 100644 --- a/test/MigrationVestingVaultTest.t.sol +++ b/test/MigrationVestingVaultTest.t.sol @@ -58,7 +58,6 @@ contract MigrationVestingVaultTest is Test { ); // Deploy migration vault - console.log("test: 1"); uint256 globalExpiration = block.number + (VESTING_DURATION / 12); vault = new MigrationVestingVault( IERC20(address(hdToken)), // Cast HDToken to IERC20 @@ -69,21 +68,18 @@ contract MigrationVestingVaultTest is Test { ); // FIXME: This is a bit janky. It would be better to use the real timelock. vault.initialize(deployer, deployer); - console.log("test: 2"); // Add vault to CoreVoting vm.startPrank(CORE_VOTING.owner()); CORE_VOTING.changeVaultStatus(address(vault), true); - console.log("test: 3"); // FIXME: This is janky. It would be better if the vault pulled directly // from an address. // // Fund vault with HD tokens for migration vm.startPrank(deployer); - hdToken.approve(address(vault), hdToken.totalSupply()); - vault.deposit(hdToken.totalSupply()); - console.log("test: 4"); + hdToken.approve(address(vault), 1_000_000e18); + vault.deposit(1_000_000e18); // Fund the addresses with ELFI. vm.startPrank(ELFI_WHALE); @@ -101,11 +97,10 @@ contract MigrationVestingVaultTest is Test { // Migration Tests // ============================== - /// @dev Ensures migration fails when destination already has a grant + /// @dev Ensures migration fails when destination already has a grant. function test_migrate_failure_existingGrant() external { // First migration uint256 amount = 100e18; - vm.startPrank(alice); ELFI.approve(address(vault), amount); vault.migrate(amount, alice); @@ -116,20 +111,20 @@ contract MigrationVestingVaultTest is Test { vm.stopPrank(); } - /// @dev Ensures migration fails when ELFI transfer fails + /// @dev Ensures migration fails when ELFI transfer fails. function test_migrate_failure_transferFailed() external { + // Try transferring ELFI without setting an approval. This should + // fail. uint256 amount = 100e18; vm.startPrank(alice); - ELFI.approve(address(vault), amount); - - vm.expectRevert(MigrationVestingVault.ElfiTransferFailed.selector); + vm.expectRevert(); vault.migrate(amount, alice); vm.stopPrank(); } /// @dev Ensures migration fails when insufficient HD tokens are available function test_migrate_failure_insufficientHDTokens() external { - uint256 excessiveAmount = hdToken.balanceOf(address(vault)) + 1; + uint256 excessiveAmount = hdToken.balanceOf(address(vault)) / vault.conversionMultiplier() + 1; vm.startPrank(alice); ELFI.approve(address(vault), excessiveAmount); @@ -215,7 +210,9 @@ contract MigrationVestingVaultTest is Test { // Delegate to bob vm.expectEmit(true, true, true, true); - emit VoteChange(alice, bob, int256(uint256(amount * CONVERSION_MULTIPLIER))); + emit VoteChange(alice, alice, -int256(uint256(amount * CONVERSION_MULTIPLIER))); + vm.expectEmit(true, true, true, true); + emit VoteChange(bob, alice, int256(uint256(amount * CONVERSION_MULTIPLIER))); vault.delegate(bob); vm.stopPrank(); @@ -242,9 +239,12 @@ contract MigrationVestingVaultTest is Test { ELFI.approve(address(vault), amount); vault.migrate(amount, alice); vm.stopPrank(); + { + uint256 votingPower = vault.queryVotePower(alice, block.number, ""); + } // Move to middle of vesting period - uint256 halfwayBlock = vault.globalExpiration() / 2; + uint256 halfwayBlock = (block.number + vault.globalExpiration()) / 2; vm.roll(halfwayBlock); // Check voting power is maintained through vesting From 9c90c821a943a8da5530c10811280688a7c2bfac Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 12 Feb 2025 15:16:13 -1000 Subject: [PATCH 5/7] Pull from the treasury instead of having to deposit HD --- src/MigrationVestingVault.sol | 18 +++++--- test/MigrationVestingVaultTest.t.sol | 67 +++++++++++++++++++--------- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/MigrationVestingVault.sol b/src/MigrationVestingVault.sol index e01c86c..5362696 100644 --- a/src/MigrationVestingVault.sol +++ b/src/MigrationVestingVault.sol @@ -18,6 +18,9 @@ import { AbstractVestingVault } from "council/vaults/VestingVault.sol"; // // FIXME: What should happen to the ELFI? Should it be burned? // +// FIXME: We should avoid setting the manager roll. We don't want grants to be +// revoked. +// /// @title MigrationVestingVault /// @notice A migration vault that converts ELFI tokens to HD tokens. Migrated /// tokens are granted with a linear vesting schedule of three months. @@ -36,6 +39,9 @@ contract MigrationVestingVault is AbstractVestingVault { /// @dev Thrown when there are insufficient HD tokens. error InsufficientHDTokens(); + /// @dev The HD treasury that is funding this migration contract. + address public immutable hdTreasury; + /// @dev The ELFI token to migrate from. IERC20 public immutable elfiToken; @@ -46,18 +52,22 @@ contract MigrationVestingVault is AbstractVestingVault { uint256 public immutable globalExpiration; /// @notice Constructs the migration vault. + /// @param _hdTreasury The HD treasury that is funding this migration + /// contract. /// @param _hdToken The ERC20 token to be vested (HD token). /// @param _elfiToken The ERC20 token to migrate from (ELFI token). /// @param _stale The stale block lag used in voting power calculations. /// @param _conversionMultiplier The conversion multiplier from ELFI to HD. /// @param _globalExpiration The global expiration block for all grants. constructor( + address _hdTreasury, IERC20 _hdToken, IERC20 _elfiToken, uint256 _stale, uint256 _conversionMultiplier, uint256 _globalExpiration ) AbstractVestingVault(_hdToken, _stale) { + hdTreasury = _hdTreasury; elfiToken = _elfiToken; conversionMultiplier = _conversionMultiplier; globalExpiration = _globalExpiration; @@ -83,9 +93,8 @@ contract MigrationVestingVault is AbstractVestingVault { // Calculate the HD token amount to be granted. uint256 hdAmount = amount * conversionMultiplier; - // Ensure sufficient HD tokens are available in the unassigned pool. - Storage.Uint256 storage unassigned = _unassigned(); - if (unassigned.data < hdAmount) { + // Pull the HD tokens from the source. + if (!token.transferFrom(hdTreasury, address(this), hdAmount)) { revert InsufficientHDTokens(); } @@ -111,9 +120,6 @@ contract MigrationVestingVault is AbstractVestingVault { range: [uint256(0), uint256(0)] }); - // Deduct the granted tokens from the unassigned pool. - unassigned.data -= hdAmount; - // Update the destination's voting power. History.HistoricalBalances memory votingPower = History.load("votingPower"); votingPower.push(destination, initialVotingPower); diff --git a/test/MigrationVestingVaultTest.t.sol b/test/MigrationVestingVaultTest.t.sol index 063e810..ac32d0e 100644 --- a/test/MigrationVestingVaultTest.t.sol +++ b/test/MigrationVestingVaultTest.t.sol @@ -18,7 +18,7 @@ contract MigrationVestingVaultTest is Test { /// @dev Constants for mainnet contracts and configuration uint256 internal constant FORK_BLOCK = 19_000_000; uint256 internal constant STALE_BLOCKS = 100; - uint256 internal constant CONVERSION_MULTIPLIER = 1; + uint256 internal constant CONVERSION_MULTIPLIER = 10; uint256 internal constant VESTING_DURATION = 90 days; address internal constant ELFI_WHALE = 0x6De73946eab234F1EE61256F10067D713aF0e37A; @@ -60,26 +60,22 @@ contract MigrationVestingVaultTest is Test { // Deploy migration vault uint256 globalExpiration = block.number + (VESTING_DURATION / 12); vault = new MigrationVestingVault( + deployer, IERC20(address(hdToken)), // Cast HDToken to IERC20 ELFI, STALE_BLOCKS, CONVERSION_MULTIPLIER, globalExpiration ); - // FIXME: This is a bit janky. It would be better to use the real timelock. vault.initialize(deployer, deployer); // Add vault to CoreVoting vm.startPrank(CORE_VOTING.owner()); CORE_VOTING.changeVaultStatus(address(vault), true); - // FIXME: This is janky. It would be better if the vault pulled directly - // from an address. - // - // Fund vault with HD tokens for migration + // Set an approval on the vault to spend 1_000_000 HD tokens. vm.startPrank(deployer); hdToken.approve(address(vault), 1_000_000e18); - vault.deposit(1_000_000e18); // Fund the addresses with ELFI. vm.startPrank(ELFI_WHALE); @@ -124,12 +120,12 @@ contract MigrationVestingVaultTest is Test { /// @dev Ensures migration fails when insufficient HD tokens are available function test_migrate_failure_insufficientHDTokens() external { - uint256 excessiveAmount = hdToken.balanceOf(address(vault)) / vault.conversionMultiplier() + 1; + uint256 excessiveAmount = hdToken.allowance(deployer, address(vault)) / vault.conversionMultiplier() + 1; vm.startPrank(alice); ELFI.approve(address(vault), excessiveAmount); - vm.expectRevert(MigrationVestingVault.InsufficientHDTokens.selector); + vm.expectRevert(); vault.migrate(excessiveAmount, alice); vm.stopPrank(); } @@ -142,26 +138,25 @@ contract MigrationVestingVaultTest is Test { uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + + // Alice migrates some of her ELFI tokens. She sets Bob as the destination + // address. vm.startPrank(alice); ELFI.approve(address(vault), amount); - - // Expect Transfer events vm.expectEmit(true, true, true, true); emit Transfer(alice, address(vault), amount); - - vault.migrate(amount, alice); + vault.migrate(amount, bob); vm.stopPrank(); - // Verify grant parameters - VestingVaultStorage.Grant memory grant = vault.getGrant(alice); - + // Ensure that the grant was properly configured. + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); assertEq(grant.withdrawn, 0, "Should not have withdrawals"); assertEq(grant.cliff, grant.created, "Cliff should equal creation block"); assertEq(grant.expiration, vault.globalExpiration(), "Wrong expiration"); - assertEq(grant.delegatee, alice, "Wrong delegatee"); + assertEq(grant.delegatee, bob, "Wrong delegatee"); - // Verify token transfers + // Verify token transfers. assertEq( ELFI.balanceOf(address(vault)), vaultElfiBalanceBefore + amount, @@ -172,6 +167,39 @@ contract MigrationVestingVaultTest is Test { aliceElfiBalanceBefore - amount, "Alice ELFI balance not updated" ); + + // Half of the three months passes, and half of the HD tokens can be + // claimed by Bob. + uint256 bobHDBalanceBefore = hdToken.balanceOf(address(bob)); + uint256 vaultHDBalanceBefore = hdToken.balanceOf(address(vault)); + uint256 votingPowerBefore = vault.queryVotePower(bob, block.number, ""); + vm.startPrank(bob); + uint256 halfwayBlock = (block.number + vault.globalExpiration()) / 2; + vm.roll(halfwayBlock); + vault.claim(); + + // Ensure that Bob received half of the HD grant and that his voting + // power was reduced by half. + uint256 bobHDBalanceAfter = hdToken.balanceOf(address(bob)); + uint256 vaultHDBalanceAfter = hdToken.balanceOf(address(vault)); + uint256 votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHDBalanceAfter, bobHDBalanceBefore + amount * CONVERSION_MULTIPLIER / 2, "Bob HD balance not updated"); + assertEq(vaultHDBalanceAfter, vaultHDBalanceBefore - amount * CONVERSION_MULTIPLIER / 2, "Bob HD balance not updated"); + assertEq(votingPowerAfter, votingPowerBefore / 2, "Voting power not updated"); + + // The other half of the three months passes.. + vm.startPrank(bob); + vm.roll(vault.globalExpiration()); + vault.claim(); + + // Ensure that Bob received the other half of the HD grant and that his + // voting power was reduced to zero. + bobHDBalanceAfter = hdToken.balanceOf(address(bob)); + vaultHDBalanceAfter = hdToken.balanceOf(address(vault)); + votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHDBalanceAfter, amount * CONVERSION_MULTIPLIER, "Bob HD balance not updated"); + assertEq(vaultHDBalanceAfter, 0, "Bob HD balance not updated"); + assertEq(votingPowerAfter, 0, "Voting power not updated"); } // ============================== @@ -239,9 +267,6 @@ contract MigrationVestingVaultTest is Test { ELFI.approve(address(vault), amount); vault.migrate(amount, alice); vm.stopPrank(); - { - uint256 votingPower = vault.queryVotePower(alice, block.number, ""); - } // Move to middle of vesting period uint256 halfwayBlock = (block.number + vault.globalExpiration()) / 2; From e10bd362d8057a35bd5780b2164fe73411dea6b4 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 12 Feb 2025 15:25:15 -1000 Subject: [PATCH 6/7] Adds some more tests --- test/MigrationVestingVaultTest.t.sol | 133 ++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 3 deletions(-) diff --git a/test/MigrationVestingVaultTest.t.sol b/test/MigrationVestingVaultTest.t.sol index ac32d0e..ecf28e2 100644 --- a/test/MigrationVestingVaultTest.t.sol +++ b/test/MigrationVestingVaultTest.t.sol @@ -130,8 +130,8 @@ contract MigrationVestingVaultTest is Test { vm.stopPrank(); } - /// @dev Ensures successful migration with correct grant creation - function test_migrate_success() external { + /// @dev Ensures successful migration with correct grant creation. + function test_migrate_and_claim() external { uint256 amount = 100e18; // Record initial states @@ -187,7 +187,7 @@ contract MigrationVestingVaultTest is Test { assertEq(vaultHDBalanceAfter, vaultHDBalanceBefore - amount * CONVERSION_MULTIPLIER / 2, "Bob HD balance not updated"); assertEq(votingPowerAfter, votingPowerBefore / 2, "Voting power not updated"); - // The other half of the three months passes.. + // The other half of the three months passes. vm.startPrank(bob); vm.roll(vault.globalExpiration()); vault.claim(); @@ -202,6 +202,133 @@ contract MigrationVestingVaultTest is Test { assertEq(votingPowerAfter, 0, "Voting power not updated"); } + /// @dev Ensures that a user can immediately claim half of their HD if they + /// migrate one and half months after the vault's time starts. + function test_migrate_and_claim_halfway_through() external { + uint256 amount = 100e18; + + // Record initial states + uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); + uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + + // Alice migrates some of her ELFI tokens. She sets Bob as the destination + // address. + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(vault), amount); + vault.migrate(amount, bob); + vm.stopPrank(); + + // Half of the three months passes and Alice migrates her ELFI. + uint256 halfwayBlock = (block.number + vault.globalExpiration()) / 2; + vm.roll(halfwayBlock); + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); + assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); + assertEq(grant.withdrawn, 0, "Should not have withdrawals"); + assertEq(grant.cliff, grant.created, "Cliff should equal creation block"); + assertEq(grant.expiration, vault.globalExpiration(), "Wrong expiration"); + assertEq(grant.delegatee, bob, "Wrong delegatee"); + + // Verify token transfers. + assertEq( + ELFI.balanceOf(address(vault)), + vaultElfiBalanceBefore + amount, + "Vault ELFI balance not updated" + ); + assertEq( + ELFI.balanceOf(alice), + aliceElfiBalanceBefore - amount, + "Alice ELFI balance not updated" + ); + + // Bob can claim half of the tokens immediately. + uint256 bobHDBalanceBefore = hdToken.balanceOf(address(bob)); + uint256 vaultHDBalanceBefore = hdToken.balanceOf(address(vault)); + uint256 votingPowerBefore = vault.queryVotePower(bob, block.number, ""); + vm.startPrank(bob); + vault.claim(); + + // Ensure that Bob received half of the HD grant and that his voting + // power was reduced by half. + uint256 bobHDBalanceAfter = hdToken.balanceOf(address(bob)); + uint256 vaultHDBalanceAfter = hdToken.balanceOf(address(vault)); + uint256 votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHDBalanceAfter, bobHDBalanceBefore + amount * CONVERSION_MULTIPLIER / 2, "Bob HD balance not updated"); + assertEq(vaultHDBalanceAfter, vaultHDBalanceBefore - amount * CONVERSION_MULTIPLIER / 2, "Bob HD balance not updated"); + assertEq(votingPowerAfter, votingPowerBefore / 2, "Voting power not updated"); + + // The other half of the three months passes. + vm.startPrank(bob); + vm.roll(vault.globalExpiration()); + vault.claim(); + + // Ensure that Bob received the other half of the HD grant and that his + // voting power was reduced to zero. + bobHDBalanceAfter = hdToken.balanceOf(address(bob)); + vaultHDBalanceAfter = hdToken.balanceOf(address(vault)); + votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHDBalanceAfter, amount * CONVERSION_MULTIPLIER, "Bob HD balance not updated"); + assertEq(vaultHDBalanceAfter, 0, "Bob HD balance not updated"); + assertEq(votingPowerAfter, 0, "Voting power not updated"); + } + + /// @dev Ensures that a user can immediately claim all of their HD if they + /// migrate four months after the vault's time starts. + function test_migrate_and_claim_after_expiration() external { + uint256 amount = 100e18; + + // Record initial states + uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); + uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + + // Alice migrates some of her ELFI tokens. She sets Bob as the destination + // address. + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(vault), amount); + vault.migrate(amount, bob); + vm.stopPrank(); + + // Four months passes and Alice migrates her ELFI. + vm.roll(vault.globalExpiration()); + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); + assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); + assertEq(grant.withdrawn, 0, "Should not have withdrawals"); + assertEq(grant.cliff, grant.created, "Cliff should equal creation block"); + assertEq(grant.expiration, vault.globalExpiration(), "Wrong expiration"); + assertEq(grant.delegatee, bob, "Wrong delegatee"); + + // Verify token transfers. + assertEq( + ELFI.balanceOf(address(vault)), + vaultElfiBalanceBefore + amount, + "Vault ELFI balance not updated" + ); + assertEq( + ELFI.balanceOf(alice), + aliceElfiBalanceBefore - amount, + "Alice ELFI balance not updated" + ); + + // Bob can claim all of the tokens immediately. + uint256 bobHDBalanceBefore = hdToken.balanceOf(address(bob)); + uint256 vaultHDBalanceBefore = hdToken.balanceOf(address(vault)); + uint256 votingPowerBefore = vault.queryVotePower(bob, block.number, ""); + vm.startPrank(bob); + vault.claim(); + + // Ensure that Bob received all of the HD grant and that his voting + // power was reduced to zero. + uint256 bobHDBalanceAfter = hdToken.balanceOf(address(bob)); + uint256 vaultHDBalanceAfter = hdToken.balanceOf(address(vault)); + uint256 votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHDBalanceAfter, bobHDBalanceBefore + amount * CONVERSION_MULTIPLIER, "Bob HD balance not updated"); + assertEq(vaultHDBalanceAfter, vaultHDBalanceBefore - amount * CONVERSION_MULTIPLIER, "Bob HD balance not updated"); + assertEq(votingPowerAfter, 0, "Voting power not updated"); + } + // ============================== // Voting Power Tests // ============================== From c5eb1f07e92d4b2cd4ea4c973b39f028c8ad269c Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 14 Feb 2025 13:38:02 -1000 Subject: [PATCH 7/7] Addressed review feedback from @jrhea and @Sean392 --- src/MigrationVestingVault.sol | 49 ++++++++++------------------ test/MigrationVestingVaultTest.t.sol | 35 ++++++++++---------- 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/src/MigrationVestingVault.sol b/src/MigrationVestingVault.sol index 5362696..2cca363 100644 --- a/src/MigrationVestingVault.sol +++ b/src/MigrationVestingVault.sol @@ -8,24 +8,11 @@ import { VestingVaultStorage } from "council/libraries/VestingVaultStorage.sol"; import { Storage } from "council/libraries/Storage.sol"; import { AbstractVestingVault } from "council/vaults/VestingVault.sol"; -// FIXME: Scrutinize this. -// -// FIXME: Test this. -// -// FIXME: We can retrofit this to pull funds from the treasury instead of needing -// to be funded directly by the treasury. This would be more flexible since we'll -// have two migration vaults. -// -// FIXME: What should happen to the ELFI? Should it be burned? -// -// FIXME: We should avoid setting the manager roll. We don't want grants to be -// revoked. -// /// @title MigrationVestingVault /// @notice A migration vault that converts ELFI tokens to HD tokens. Migrated -/// tokens are granted with a linear vesting schedule of three months. -/// The grant is created at a destination address provided by the -/// migrator. This contract inherits full voting power tracking from +/// tokens are granted with a linear vesting schedule. The grant is +/// created at a destination address provided by the migrator. This +/// contract inherits full voting power tracking from /// `AbstractVestingVault`. contract MigrationVestingVault is AbstractVestingVault { using History for History.HistoricalBalances; @@ -45,11 +32,14 @@ contract MigrationVestingVault is AbstractVestingVault { /// @dev The ELFI token to migrate from. IERC20 public immutable elfiToken; - // The conversion rate from ELFI to HD. + /// @dev The conversion rate from ELFI to HD. uint256 public immutable conversionMultiplier; - // The global expiration block at which all grants fully vest. - uint256 public immutable globalExpiration; + /// @dev The global start block at which all grants start vesting. + uint256 public immutable startBlock; + + /// @dev The global expiration block at which all grants fully vest. + uint256 public immutable expiration; /// @notice Constructs the migration vault. /// @param _hdTreasury The HD treasury that is funding this migration @@ -58,19 +48,22 @@ contract MigrationVestingVault is AbstractVestingVault { /// @param _elfiToken The ERC20 token to migrate from (ELFI token). /// @param _stale The stale block lag used in voting power calculations. /// @param _conversionMultiplier The conversion multiplier from ELFI to HD. - /// @param _globalExpiration The global expiration block for all grants. + /// @param _startBlock The global start block for all grants. + /// @param _expiration The global expiration block for all grants. constructor( address _hdTreasury, IERC20 _hdToken, IERC20 _elfiToken, uint256 _stale, uint256 _conversionMultiplier, - uint256 _globalExpiration + uint256 _startBlock, + uint256 _expiration ) AbstractVestingVault(_hdToken, _stale) { hdTreasury = _hdTreasury; elfiToken = _elfiToken; conversionMultiplier = _conversionMultiplier; - globalExpiration = _globalExpiration; + startBlock = _startBlock; + expiration = _expiration; } /// @notice Migrates a specified amount of ELFI tokens into a vesting grant of HD tokens. @@ -98,12 +91,6 @@ contract MigrationVestingVault is AbstractVestingVault { revert InsufficientHDTokens(); } - // Set the vesting parameters. We use the global expiration for all - // grants, and the vesting starts immediately. - uint128 startBlock = uint128(block.number); - uint128 expiration = uint128(globalExpiration); - uint128 cliff = startBlock; - // Calculate the initial voting power using the current unvested multiplier. Storage.Uint256 memory unvestedMultiplier = _unvestedMultiplier(); uint128 initialVotingPower = uint128((hdAmount * uint128(unvestedMultiplier.data)) / 100); @@ -112,9 +99,9 @@ contract MigrationVestingVault is AbstractVestingVault { _grants()[destination] = VestingVaultStorage.Grant({ allocation: uint128(hdAmount), withdrawn: 0, - created: startBlock, - expiration: expiration, - cliff: cliff, + created: uint128(startBlock), + expiration: uint128(expiration), + cliff: uint128(startBlock), // vesting starts immediately latestVotingPower: initialVotingPower, delegatee: destination, range: [uint256(0), uint256(0)] diff --git a/test/MigrationVestingVaultTest.t.sol b/test/MigrationVestingVaultTest.t.sol index ecf28e2..e77c07a 100644 --- a/test/MigrationVestingVaultTest.t.sol +++ b/test/MigrationVestingVaultTest.t.sol @@ -58,14 +58,16 @@ contract MigrationVestingVaultTest is Test { ); // Deploy migration vault - uint256 globalExpiration = block.number + (VESTING_DURATION / 12); + uint256 startBlock = block.number; + uint256 expiration = block.number + (VESTING_DURATION / 12); vault = new MigrationVestingVault( deployer, IERC20(address(hdToken)), // Cast HDToken to IERC20 ELFI, STALE_BLOCKS, CONVERSION_MULTIPLIER, - globalExpiration + startBlock, + expiration ); vault.initialize(deployer, deployer); @@ -153,7 +155,7 @@ contract MigrationVestingVaultTest is Test { assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); assertEq(grant.withdrawn, 0, "Should not have withdrawals"); assertEq(grant.cliff, grant.created, "Cliff should equal creation block"); - assertEq(grant.expiration, vault.globalExpiration(), "Wrong expiration"); + assertEq(grant.expiration, vault.expiration(), "Wrong expiration"); assertEq(grant.delegatee, bob, "Wrong delegatee"); // Verify token transfers. @@ -174,7 +176,7 @@ contract MigrationVestingVaultTest is Test { uint256 vaultHDBalanceBefore = hdToken.balanceOf(address(vault)); uint256 votingPowerBefore = vault.queryVotePower(bob, block.number, ""); vm.startPrank(bob); - uint256 halfwayBlock = (block.number + vault.globalExpiration()) / 2; + uint256 halfwayBlock = (block.number + vault.expiration()) / 2; vm.roll(halfwayBlock); vault.claim(); @@ -189,7 +191,7 @@ contract MigrationVestingVaultTest is Test { // The other half of the three months passes. vm.startPrank(bob); - vm.roll(vault.globalExpiration()); + vm.roll(vault.expiration()); vault.claim(); // Ensure that Bob received the other half of the HD grant and that his @@ -211,6 +213,10 @@ contract MigrationVestingVaultTest is Test { uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + // Half of the three months passes and Alice migrates her ELFI. + uint256 halfwayBlock = (block.number + vault.expiration()) / 2; + vm.roll(halfwayBlock); + // Alice migrates some of her ELFI tokens. She sets Bob as the destination // address. vm.startPrank(alice); @@ -219,15 +225,11 @@ contract MigrationVestingVaultTest is Test { emit Transfer(alice, address(vault), amount); vault.migrate(amount, bob); vm.stopPrank(); - - // Half of the three months passes and Alice migrates her ELFI. - uint256 halfwayBlock = (block.number + vault.globalExpiration()) / 2; - vm.roll(halfwayBlock); VestingVaultStorage.Grant memory grant = vault.getGrant(bob); assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); assertEq(grant.withdrawn, 0, "Should not have withdrawals"); assertEq(grant.cliff, grant.created, "Cliff should equal creation block"); - assertEq(grant.expiration, vault.globalExpiration(), "Wrong expiration"); + assertEq(grant.expiration, vault.expiration(), "Wrong expiration"); assertEq(grant.delegatee, bob, "Wrong delegatee"); // Verify token transfers. @@ -260,7 +262,7 @@ contract MigrationVestingVaultTest is Test { // The other half of the three months passes. vm.startPrank(bob); - vm.roll(vault.globalExpiration()); + vm.roll(vault.expiration()); vault.claim(); // Ensure that Bob received the other half of the HD grant and that his @@ -282,6 +284,9 @@ contract MigrationVestingVaultTest is Test { uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + // Four months passes and Alice migrates her ELFI. + vm.roll(vault.expiration()); + // Alice migrates some of her ELFI tokens. She sets Bob as the destination // address. vm.startPrank(alice); @@ -290,14 +295,11 @@ contract MigrationVestingVaultTest is Test { emit Transfer(alice, address(vault), amount); vault.migrate(amount, bob); vm.stopPrank(); - - // Four months passes and Alice migrates her ELFI. - vm.roll(vault.globalExpiration()); VestingVaultStorage.Grant memory grant = vault.getGrant(bob); assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); assertEq(grant.withdrawn, 0, "Should not have withdrawals"); assertEq(grant.cliff, grant.created, "Cliff should equal creation block"); - assertEq(grant.expiration, vault.globalExpiration(), "Wrong expiration"); + assertEq(grant.expiration, vault.expiration(), "Wrong expiration"); assertEq(grant.delegatee, bob, "Wrong delegatee"); // Verify token transfers. @@ -315,7 +317,6 @@ contract MigrationVestingVaultTest is Test { // Bob can claim all of the tokens immediately. uint256 bobHDBalanceBefore = hdToken.balanceOf(address(bob)); uint256 vaultHDBalanceBefore = hdToken.balanceOf(address(vault)); - uint256 votingPowerBefore = vault.queryVotePower(bob, block.number, ""); vm.startPrank(bob); vault.claim(); @@ -396,7 +397,7 @@ contract MigrationVestingVaultTest is Test { vm.stopPrank(); // Move to middle of vesting period - uint256 halfwayBlock = (block.number + vault.globalExpiration()) / 2; + uint256 halfwayBlock = (block.number + vault.expiration()) / 2; vm.roll(halfwayBlock); // Check voting power is maintained through vesting