diff --git a/lib/council b/lib/council index 5f7be33..3003e56 160000 --- a/lib/council +++ b/lib/council @@ -1 +1 @@ -Subproject commit 5f7be330b05f1c3bebd0176882cc5c3429f0764f +Subproject commit 3003e568a8d35e9fa7b59fcb5e961b2c577f1d0c diff --git a/src/MigrationRewardsVault.sol b/src/MigrationRewardsVault.sol new file mode 100644 index 0000000..e7f5c3c --- /dev/null +++ b/src/MigrationRewardsVault.sol @@ -0,0 +1,304 @@ +// 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"; + +/// @title MigrationRewardsVault +/// @notice A migration vault that converts ELFI tokens to HD tokens. Migrated +/// tokens have a cliff. After this cliff, they will accrue a bonus +/// linearly as time passes. The grant is created at a destination +/// address provided by the migrator. This contract inherits full voting +/// power tracking from `AbstractVestingVault`. +contract MigrationRewardsVault is AbstractVestingVault { + using History for History.HistoricalBalances; + + /// @notice Thrown when an existing grant is found. + error ExistingGrantFound(); + + /// @notice Thrown when ELFI transfers fail. + error ElfiTransferFailed(); + + /// @notice Thrown when there are insufficient HD tokens. + error InsufficientHDTokens(); + + /// @notice Thrown when the destination is zero. + error InvalidDestination(); + + /// @notice Thrown when the migration amount is zero. + error InvalidMigrationAmount(); + + /// @notice Thrown when no tokens are withdrawable during a claim attempt. + error NothingToClaim(); + + /// @notice Thrown when the HD token transfer to the claimant fails. + error TransferFailed(); + + /// @notice Thrown when the HD token transfer to the treasury fails. + error TreasuryTransferFailed(); + + /// @notice One in basis points. + uint256 public constant ONE = 1e18; + + /// @notice The conversion rate from ELFI to HD. + uint256 public constant CONVERSION_MULTIPLIER = 10; + + /// @notice The bonus multiplier, representing a 5% APR over + /// a three-month cliff period. For a 5% APR over 2 months (0.16 + /// years), bonus = 5% * 0.16 which is approximately 0.83%. + uint256 public constant BONUS_MULTIPLIER = 1.008333333333333333e18; + + /// @notice The number of blocks between deploying the contract and the + /// vesting cliff. + uint256 public constant CLIFF_DURATION = 91 days / 12; // ~3 months + + /// @notice The number of blocks between deploying the contract and the + /// expiration. + uint256 public constant EXPIRATION_DURATION = 152 days / 12; // ~5 months + + /// @notice The HD treasury that is funding this migration contract. + address public immutable hdTreasury; + + /// @notice The ELFI token to migrate from. + IERC20 public immutable elfiToken; + + /// @notice The global start block at which all grants start vesting. + uint256 public immutable startBlock; + + /// @notice The global cliff block at which all grants have vested their cliff + /// amount. + uint256 public immutable cliff; + + /// @notice The global expiration block at which all grants fully vest. + uint256 public immutable expiration; + + /// @notice Constructs the migration vault. + /// @param _hdTreasury The HD treasury 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 for voting power calculations. + constructor( + address _hdTreasury, + IERC20 _hdToken, + IERC20 _elfiToken, + uint256 _stale + ) AbstractVestingVault(_hdToken, _stale) { + // Set immutable variables + hdTreasury = _hdTreasury; + elfiToken = _elfiToken; + + // Use deployment block as startBlock. + startBlock = block.number; + + // Calculate cliff and expiration based on durations. + cliff = block.number + CLIFF_DURATION; + expiration = block.number + EXPIRATION_DURATION; + } + + /// @notice Migrates a specified amount of ELFI tokens into a vesting grant + /// of HD tokens. + /// @dev Converts ELFI to HD at the conversion rate. Pre-cliff migrations + /// receive a 5% bonus vesting post-cliff over 2 months. Post-cliff, + /// pre-expiration migrations receive a reduced bonus proportional to + /// remaining time, vesting from creation. Post-expiration migrations + /// receive no bonus, just the base amount. Caller must approve this + /// contract for ELFI tokens, and the treasury must approve HD tokens. + /// @param _amount The amount of ELFI tokens to migrate. + /// @param _destination The address to receive the HD token grant. + function migrate(uint256 _amount, address _destination) external { + // If the amount is zero, we shouldn't proceed with the migration. + if (_amount == 0) { + revert InvalidMigrationAmount(); + } + + // If the destination is zero, we shouldn't proceed with the migration. + if (_destination == address(0)) { + revert InvalidDestination(); + } + + // Prevent duplicate grants at the destination. + VestingVaultStorage.Grant storage existingGrant = _grants()[_destination]; + if (existingGrant.allocation != 0) { + revert ExistingGrantFound(); + } + + // Transfer ELFI tokens from the caller to this contract. + if (!elfiToken.transferFrom(msg.sender, address(this), _amount)) { + revert ElfiTransferFailed(); + } + + // Calculate the base HD amount from ELFI conversion. + uint256 baseHdAmount = _amount * CONVERSION_MULTIPLIER; + uint256 totalHdAmount; + + // Determine the total HD amount based on migration timing. + if (block.number < cliff) { + // Full 5% bonus for pre-cliff migrations. + totalHdAmount = (baseHdAmount * BONUS_MULTIPLIER) / ONE; + } else if (block.number < expiration) { + // Reduced bonus for post-cliff, pre-expiration migrations. + uint256 blocksRemaining = expiration - block.number; + uint256 bonusPeriod = expiration - cliff; + uint256 bonusFactor = ONE + ((BONUS_MULTIPLIER - ONE) * blocksRemaining) / bonusPeriod; + totalHdAmount = (baseHdAmount * bonusFactor) / ONE; + } else { + // No bonus for post-expiration migrations, just base amount. + totalHdAmount = baseHdAmount; + } + + // Transfer HD tokens from the treasury to this contract. + if (!token.transferFrom(hdTreasury, address(this), totalHdAmount)) { + revert InsufficientHDTokens(); + } + + // Create the grant with current block as creation time and base as initial voting power. + _grants()[_destination] = VestingVaultStorage.Grant({ + allocation: uint128(totalHdAmount), + withdrawn: 0, + created: uint128(block.number), + expiration: uint128(expiration), + cliff: uint128(cliff), + latestVotingPower: uint128(baseHdAmount), + delegatee: _destination, + range: [uint256(0), uint256(0)] + }); + + // Update voting power history with the base amount. + History.HistoricalBalances memory votingPower = _votingPower(); + votingPower.push(_destination, baseHdAmount); + emit VoteChange(_destination, _destination, int256(baseHdAmount)); + } + + /// @notice Claims all withdrawable HD tokens from the caller's grant and + /// terminates it. + /// @dev Withdraws the currently withdrawable amount (base plus vested + /// bonus), returns any unvested bonus to the treasury, resets voting + /// power to 0, and deletes the grant. Fails if no tokens are + /// withdrawable (e.g., before cliff for early migrators or before + /// creation). + function claim() public override { + // Load the caller’s grant and calculate the withdrawable amount. + VestingVaultStorage.Grant storage grant = _grants()[msg.sender]; + uint256 withdrawable = _getWithdrawableAmount(grant); + if (withdrawable == 0) { + revert NothingToClaim(); + } + + // Calculate the unvested amount to return to the treasury. + uint256 unvested = grant.allocation > withdrawable ? grant.allocation - withdrawable : 0; + + // Transfer withdrawable amount to the claimant. + if (!token.transfer(msg.sender, withdrawable)) { + revert TransferFailed(); + } + + // Return any unvested bonus to the treasury. + if (unvested > 0) { + if (!token.transfer(hdTreasury, unvested)) { + revert TreasuryTransferFailed(); + } + } + + // Reset voting power to 0 and update delegatee’s history. + if (grant.latestVotingPower > 0) { + History.HistoricalBalances memory votingPower = _votingPower(); + uint256 delegateeVotes = votingPower.loadTop(grant.delegatee); + votingPower.push(grant.delegatee, delegateeVotes - grant.latestVotingPower); + emit VoteChange(grant.delegatee, msg.sender, -int256(uint256(grant.latestVotingPower))); + } + + // Delete the grant to prevent further vesting or claims. + delete _grants()[msg.sender]; + } + + /// @notice Calculates the current voting power of a grant. + /// @dev Returns 0 before creation. For early migrators (pre-cliff), returns + /// the base amount until the cliff, then tracks the withdrawable + /// amount. For late migrators (post-cliff), returns 0 until creation, + /// then tracks the withdrawable amount (base immediately, plus vested + /// bonus). + /// @param _grant The grant to check. + /// @return The current voting power of the grant. + function _currentVotingPower(VestingVaultStorage.Grant memory _grant) + internal + view + override + returns (uint256) + { + // No voting power before the grant is created. + if (block.number < _grant.created) { + return 0; + } + + // Before the cliff (for early migrators), use the base amount set at + // creation. + if (block.number < _grant.cliff) { + return _grant.latestVotingPower; + } + + // After the cliff (or creation for late migrators), use the + // withdrawable amount. + return _getWithdrawableAmount(_grant); + } + + /// @notice Calculates the amount of HD tokens withdrawable from a grant. + /// @dev Returns 0 before the vesting start (cliff for early migrators, + /// creation for late migrators). For early migrators (pre-cliff), the + /// base unlocks at the cliff, and the 5% bonus vests linearly over 2 + /// months. For late migrators (post-cliff), the base is immediately + /// withdrawable, and a reduced bonus vests linearly over the remaining + /// time to expiration. + /// @param _grant The grant to check. + /// @return The total withdrawable amount (base plus vested bonus, less any + /// prior withdrawals). + function _getWithdrawableAmount(VestingVaultStorage.Grant memory _grant) + internal + view + override + returns (uint256) + { + // Nothing withdrawable before creation or before cliff for early migrators. + if (block.number < _grant.created || (_grant.created < cliff && block.number < cliff)) { + return 0; + } + + // Calculate the effective bonus factor based on creation time. + uint256 effectiveBonusFactor; + if (_grant.created < cliff) { + effectiveBonusFactor = BONUS_MULTIPLIER; // Full 5% bonus (10,500) + } else { + // For late migrators, scale bonus based on remaining blocks to expiration. + uint256 blocksRemaining = _grant.expiration > _grant.created ? _grant.expiration - _grant.created : 0; + uint256 bonusPeriod = _grant.expiration - cliff; + effectiveBonusFactor = ONE + ((BONUS_MULTIPLIER - ONE) * blocksRemaining) / bonusPeriod; + } + + // Derive the base amount using the effective bonus factor. + uint256 baseAmount = (_grant.allocation * ONE) / effectiveBonusFactor; + uint256 maxBonusAmount = _grant.allocation - baseAmount; + + // Return full allocation if past expiration. + if (block.number >= _grant.expiration) { + return _grant.allocation; + } + + // Vesting starts at cliff for early migrators, creation for late migrators. + uint256 vestingStart = _grant.created < cliff ? cliff : _grant.created; + if (block.number < vestingStart) { + return 0; + } + + // Calculate vested bonus linearly from vesting start to expiration. + uint256 blocksSinceVestingStart = block.number - vestingStart; + uint256 vestingPeriod = _grant.expiration - vestingStart; + uint256 vestedBonus = (maxBonusAmount * blocksSinceVestingStart) / vestingPeriod; + + // Clamp the result to allocation to handle rounding errors. + uint256 withdrawable = baseAmount + vestedBonus; + return withdrawable > _grant.allocation ? _grant.allocation : withdrawable; + } +} diff --git a/src/MigrationVestingVault.sol b/src/MigrationVestingVault.sol index 2cca363..916d27a 100644 --- a/src/MigrationVestingVault.sol +++ b/src/MigrationVestingVault.sol @@ -26,15 +26,25 @@ contract MigrationVestingVault is AbstractVestingVault { /// @dev Thrown when there are insufficient HD tokens. error InsufficientHDTokens(); + /// @notice Thrown when the destination is zero. + error InvalidDestination(); + + /// @notice Thrown when the migration amount is zero. + error InvalidMigrationAmount(); + + /// @notice The conversion rate from ELFI to HD. + uint256 public constant CONVERSION_MULTIPLIER = 10; + + /// @notice The number of blocks between deploying the contract and the + /// expiration. + uint256 public constant EXPIRATION_DURATION = 91 days / 12; // ~3 months + /// @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; - /// @dev The conversion rate from ELFI to HD. - uint256 public immutable conversionMultiplier; - /// @dev The global start block at which all grants start vesting. uint256 public immutable startBlock; @@ -47,44 +57,47 @@ contract MigrationVestingVault is AbstractVestingVault { /// @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 _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 _startBlock, - uint256 _expiration + uint256 _stale ) AbstractVestingVault(_hdToken, _stale) { hdTreasury = _hdTreasury; elfiToken = _elfiToken; - conversionMultiplier = _conversionMultiplier; - startBlock = _startBlock; - expiration = _expiration; + startBlock = block.number; + expiration = startBlock + EXPIRATION_DURATION; } /// @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 { + /// @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 { + // If the amount is zero, we shouldn't proceed with the migration. + if (_amount == 0) { + revert InvalidMigrationAmount(); + } + + // If the destination is zero, we shouldn't proceed with the migration. + if (_destination == address(0)) { + revert InvalidDestination(); + } + // Ensure the destination does not already have an active grant. - VestingVaultStorage.Grant storage existingGrant = _grants()[destination]; + VestingVaultStorage.Grant storage existingGrant = _grants()[_destination]; if (existingGrant.allocation != 0) { revert ExistingGrantFound(); } // Transfer ELFI tokens from the caller to this contract. - if (!elfiToken.transferFrom(msg.sender, address(this), amount)) { + if (!elfiToken.transferFrom(msg.sender, address(this), _amount)) { revert ElfiTransferFailed(); } // Calculate the HD token amount to be granted. - uint256 hdAmount = amount * conversionMultiplier; + uint256 hdAmount = _amount * CONVERSION_MULTIPLIER; // Pull the HD tokens from the source. if (!token.transferFrom(hdTreasury, address(this), hdAmount)) { @@ -96,20 +109,20 @@ contract MigrationVestingVault is AbstractVestingVault { uint128 initialVotingPower = uint128((hdAmount * uint128(unvestedMultiplier.data)) / 100); // Create the grant at the destination address. - _grants()[destination] = VestingVaultStorage.Grant({ + _grants()[_destination] = VestingVaultStorage.Grant({ allocation: uint128(hdAmount), withdrawn: 0, created: uint128(startBlock), expiration: uint128(expiration), cliff: uint128(startBlock), // vesting starts immediately latestVotingPower: initialVotingPower, - delegatee: destination, + delegatee: _destination, range: [uint256(0), uint256(0)] }); // Update the destination's voting power. History.HistoricalBalances memory votingPower = History.load("votingPower"); - votingPower.push(destination, initialVotingPower); - emit VoteChange(destination, destination, int256(uint256(initialVotingPower))); + votingPower.push(_destination, initialVotingPower); + emit VoteChange(_destination, _destination, int256(uint256(initialVotingPower))); } } diff --git a/test/MigrationRewardsVaultTest.t.sol b/test/MigrationRewardsVaultTest.t.sol new file mode 100644 index 0000000..9e8c2f8 --- /dev/null +++ b/test/MigrationRewardsVaultTest.t.sol @@ -0,0 +1,525 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +import { Test } from "forge-std/Test.sol"; +import { HDToken } from "../src/HDToken.sol"; // Assuming HDToken.sol exists +import { IERC20 } from "council/interfaces/IERC20.sol"; +import { CoreVoting } from "council/CoreVoting.sol"; +import { MigrationRewardsVault } from "../src/MigrationRewardsVault.sol"; +import { VestingVaultStorage } from "council/libraries/VestingVaultStorage.sol"; + +/// @title MigrationRewardsVaultTest +/// @notice Test suite for the MigrationRewardsVault contract, covering migration, +/// vesting, bonus application, voting power tracking, and claiming. +contract MigrationRewardsVaultTest 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; + address internal constant ELFI_WHALE = 0x6De73946eab234F1EE61256F10067D713aF0e37A; + + /// @dev Contract instances + CoreVoting internal constant CORE_VOTING = CoreVoting(0xEaCD577C3F6c44C3ffA398baaD97aE12CDCFed4a); + IERC20 internal constant ELFI = IERC20(0x5c6D51ecBA4D8E4F20373e3ce96a62342B125D6d); + MigrationRewardsVault 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 by forking mainnet, deploying contracts, + /// and configuring accounts and CoreVoting. + 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 + vault = new MigrationRewardsVault( + deployer, // Treasury is deployer for simplicity + IERC20(address(hdToken)), + ELFI, + STALE_BLOCKS + ); + + // Add vault to CoreVoting + vm.startPrank(CORE_VOTING.owner()); + CORE_VOTING.changeVaultStatus(address(vault), true); + + // Approve vault to spend 1,000,000 HD tokens from deployer (treasury) + vm.startPrank(deployer); + hdToken.approve(address(vault), 1_000_000e18); + + // Fund test accounts with ELFI from whale + 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); + } + vm.stopPrank(); + } + + // ============================== + // Migration Tests + // ============================== + + /// @notice Tests that migration fails with zero migration amount. + function test_migrate_failure_zeroAmount() external { + // Set up Alice to attempt a migration with zero amount + vm.startPrank(alice); + ELFI.approve(address(vault), 1e18); // Approve some amount even though we'll send 0 + + // Expect the specific error for zero amount + vm.expectRevert(MigrationRewardsVault.InvalidMigrationAmount.selector); + + // Attempt migration with zero amount + vault.migrate(0, bob); + vm.stopPrank(); + } + + /// @notice Tests that migration fails with zero address destination. + function test_migrate_failure_zeroAddressDestination() external { + // Set up Alice to attempt a migration to the zero address + uint256 amount = 100e18; + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + + // Expect the specific error for zero address destination + vm.expectRevert(MigrationRewardsVault.InvalidDestination.selector); + + // Attempt migration to address(0) + vault.migrate(amount, address(0)); + vm.stopPrank(); + } + + /// @notice Tests that migration fails if the destination already has a grant. + function test_migrate_failure_existingGrant() external { + uint256 amount = 100e18; + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, alice); + + // Attempt second migration to same destination + vm.expectRevert(MigrationRewardsVault.ExistingGrantFound.selector); + vault.migrate(amount, alice); + vm.stopPrank(); + } + + /// @notice Tests that migration fails if ELFI transfer fails (no approval). + function test_migrate_failure_transferFailed() external { + uint256 amount = 100e18; + vm.startPrank(alice); + vm.expectRevert(); // ERC20: insufficient allowance + vault.migrate(amount, alice); + vm.stopPrank(); + } + + /// @notice Tests that migration fails if insufficient HD tokens are available. + function test_migrate_failure_insufficientHDTokens() external { + uint256 excessiveAmount = (hdToken.allowance(deployer, address(vault)) / 10) + 1e18; + vm.startPrank(alice); + ELFI.approve(address(vault), excessiveAmount); + vm.expectRevert(); + vault.migrate(excessiveAmount, alice); + vm.stopPrank(); + } + + /// @notice Tests claiming fails before the cliff. + function test_migrate_preCliff_and_claim_preCliff_failure() external { + uint256 amount = 100e18; + + // Record initial states + uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); + uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + uint256 vaultHdBalanceBefore = hdToken.balanceOf(address(vault)); + uint256 bobHdBalanceBefore = hdToken.balanceOf(bob); + + // Alice migrates ELFI to Bob (pre-cliff) + 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(); + + // Verify grant configuration + uint256 treasuryHdBalanceBefore = hdToken.balanceOf(vault.hdTreasury()); + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); + uint256 expectedAllocation = (amount * 10 * vault.BONUS_MULTIPLIER()) / vault.ONE(); + assertEq(grant.allocation, expectedAllocation, "Wrong allocation"); + assertEq(grant.withdrawn, 0, "Should not have withdrawals"); + assertEq(grant.created, block.number, "Wrong creation block"); + assertEq(grant.cliff, vault.cliff(), "Wrong cliff"); + assertEq(grant.expiration, vault.expiration(), "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"); + assertEq(hdToken.balanceOf(address(vault)), vaultHdBalanceBefore + expectedAllocation, "Vault HD balance not updated"); + + // Move to halfway between the start block and the cliff. + uint256 halfwayBlock = (vault.startBlock() + vault.cliff()) / 2; + vm.roll(halfwayBlock); + + // Bob attempts to claim. This should fail + vm.startPrank(bob); + vm.expectRevert(MigrationRewardsVault.NothingToClaim.selector); + vault.claim(); + } + + /// @notice Tests successful migration and claiming for a pre-cliff migrator. + function test_migrate_preCliff_and_claim_preExpiration() external { + uint256 amount = 100e18; + + // Record initial states + uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); + uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + uint256 vaultHdBalanceBefore = hdToken.balanceOf(address(vault)); + uint256 bobHdBalanceBefore = hdToken.balanceOf(bob); + + // Alice migrates ELFI to Bob (pre-cliff) + 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(); + + // Verify grant configuration + uint256 treasuryHdBalanceBefore = hdToken.balanceOf(vault.hdTreasury()); + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); + uint256 expectedAllocation = (amount * 10 * vault.BONUS_MULTIPLIER()) / vault.ONE(); + assertEq(grant.allocation, expectedAllocation, "Wrong allocation"); + assertEq(grant.withdrawn, 0, "Should not have withdrawals"); + assertEq(grant.created, block.number, "Wrong creation block"); + assertEq(grant.cliff, vault.cliff(), "Wrong cliff"); + assertEq(grant.expiration, vault.expiration(), "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"); + assertEq(hdToken.balanceOf(address(vault)), vaultHdBalanceBefore + expectedAllocation, "Vault HD balance not updated"); + + // Move to halfway between cliff and expiration + uint256 halfwayBlock = vault.cliff() + (vault.expiration() - vault.cliff()) / 2; + vm.roll(halfwayBlock); + + // Bob claims + vm.startPrank(bob); + vault.claim(); + + // Verify claim outcomes + uint256 bobHdBalanceAfter = hdToken.balanceOf(bob); + uint256 vaultHdBalanceAfter = hdToken.balanceOf(address(vault)); + uint256 votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + uint256 expectedBase = amount * 10; // CONVERSION_MULTIPLIER = 10e18 + uint256 expectedBonusHalf = ((expectedAllocation - expectedBase) / 2); + assertEq(bobHdBalanceAfter, bobHdBalanceBefore + expectedBase + expectedBonusHalf, "Bob HD balance incorrect"); + assertEq(vaultHdBalanceAfter, 0, "Vault HD balance incorrect"); + assertEq(votingPowerAfter, 0, "Voting power should be zero after claim"); + assertEq(hdToken.balanceOf(vault.hdTreasury()), treasuryHdBalanceBefore + expectedBonusHalf, "Treasury should receive unvested bonus"); + + // Verify grant is deleted + grant = vault.getGrant(bob); + assertEq(grant.allocation, 0, "Grant should be deleted"); + } + + /// @notice Tests successful migration pre cliff and claiming post expiration. + function test_migrate_preCliff_and_claim_postExpiration() external { + uint256 amount = 100e18; + + // Record initial states + uint256 vaultElfiBalanceBefore = ELFI.balanceOf(address(vault)); + uint256 aliceElfiBalanceBefore = ELFI.balanceOf(alice); + uint256 vaultHdBalanceBefore = hdToken.balanceOf(address(vault)); + uint256 bobHdBalanceBefore = hdToken.balanceOf(bob); + + // Alice migrates ELFI to Bob (pre-cliff) + 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(); + + // Verify grant configuration + uint256 treasuryHdBalanceBefore = hdToken.balanceOf(vault.hdTreasury()); + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); + uint256 expectedAllocation = (amount * 10 * vault.BONUS_MULTIPLIER()) / vault.ONE(); + assertEq(grant.allocation, expectedAllocation, "Wrong allocation"); + assertEq(grant.withdrawn, 0, "Should not have withdrawals"); + assertEq(grant.created, block.number, "Wrong creation block"); + assertEq(grant.cliff, vault.cliff(), "Wrong cliff"); + assertEq(grant.expiration, vault.expiration(), "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"); + assertEq(hdToken.balanceOf(address(vault)), vaultHdBalanceBefore + expectedAllocation, "Vault HD balance not updated"); + + // Move to expiration + vm.roll(vault.expiration() + 1_000); + + // Bob claims + vm.startPrank(bob); + vault.claim(); + + // Verify claim outcomes + uint256 bobHdBalanceAfter = hdToken.balanceOf(bob); + uint256 vaultHdBalanceAfter = hdToken.balanceOf(address(vault)); + uint256 votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHdBalanceAfter, bobHdBalanceBefore + expectedAllocation, "Bob HD balance incorrect"); + assertEq(vaultHdBalanceAfter, 0, "Vault HD balance incorrect"); + assertEq(votingPowerAfter, 0, "Voting power should be zero after claim"); + assertEq(hdToken.balanceOf(vault.hdTreasury()), treasuryHdBalanceBefore, "Treasury should receive unvested bonus"); + + // Verify grant is deleted + grant = vault.getGrant(bob); + assertEq(grant.allocation, 0, "Grant should be deleted"); + } + + /// @notice Tests migration and immediate partial claim for a post-cliff migrator. + function test_migrate_postCliff_and_claim_preExpiration() external { + uint256 amount = 100e18; + + // Move to halfway between cliff and expiration + uint256 halfwayBlock = vault.cliff() + (vault.expiration() - vault.cliff()) / 2; + vm.roll(halfwayBlock); + + // Record initial states + uint256 bobHdBalanceBefore = hdToken.balanceOf(bob); + + // Alice migrates ELFI to Bob (post-cliff) + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, bob); + vm.stopPrank(); + + // Verify grant configuration + uint256 treasuryHdBalanceBefore = hdToken.balanceOf(vault.hdTreasury()); + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); + uint256 blocksRemaining = vault.expiration() - halfwayBlock; + uint256 bonusPeriod = vault.expiration() - vault.cliff(); + uint256 bonusFactor = vault.ONE() + ((vault.BONUS_MULTIPLIER() - vault.ONE()) * blocksRemaining) / bonusPeriod; + uint256 expectedBase = amount * 10; // CONVERSION_MULTIPLIER = 10e18 + uint256 expectedAllocation = (expectedBase * bonusFactor) / vault.ONE(); + assertEq(grant.allocation, expectedAllocation, "Wrong allocation"); + assertEq(grant.created, halfwayBlock, "Wrong creation block"); + + // Bob claims immediately + vm.startPrank(bob); + vault.claim(); + + // Verify claim outcomes + uint256 bobHdBalanceAfter = hdToken.balanceOf(bob); + uint256 vaultHdBalanceAfter = hdToken.balanceOf(address(vault)); + uint256 votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHdBalanceAfter, bobHdBalanceBefore + expectedBase, "Bob HD balance incorrect"); + assertEq(vaultHdBalanceAfter, 0, "Vault HD balance incorrect"); + assertEq(votingPowerAfter, 0, "Voting power should be zero after claim"); + assertEq(hdToken.balanceOf(vault.hdTreasury()), treasuryHdBalanceBefore + (expectedAllocation - expectedBase), "Treasury balance incorrect"); + + // Verify grant is deleted + grant = vault.getGrant(bob); + assertEq(grant.allocation, 0, "Grant should be deleted"); + } + + /// @notice Tests migration post cliff and claiming post expiration. + function test_migrate_postCliff_and_claim_postExpiration() external { + uint256 amount = 100e18; + + // Move to halfway between cliff and expiration + uint256 halfwayBlock = vault.cliff() + (vault.expiration() - vault.cliff()) / 2; + vm.roll(halfwayBlock); + + // Record initial states + uint256 bobHdBalanceBefore = hdToken.balanceOf(bob); + + // Alice migrates ELFI to Bob (post-cliff) + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, bob); + vm.stopPrank(); + + // Verify grant configuration + uint256 treasuryHdBalanceBefore = hdToken.balanceOf(vault.hdTreasury()); + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); + uint256 blocksRemaining = vault.expiration() - halfwayBlock; + uint256 bonusPeriod = vault.expiration() - vault.cliff(); + uint256 bonusFactor = vault.ONE() + ((vault.BONUS_MULTIPLIER() - vault.ONE()) * blocksRemaining) / bonusPeriod; + uint256 expectedBase = amount * 10; // CONVERSION_MULTIPLIER = 10e18 + uint256 expectedAllocation = (expectedBase * bonusFactor) / vault.ONE(); + assertEq(grant.allocation, expectedAllocation, "Wrong allocation"); + assertEq(grant.created, halfwayBlock, "Wrong creation block"); + + // Bob claims after expiration + vm.roll(vault.expiration() + 1_000); + vm.startPrank(bob); + vault.claim(); + + // Verify claim outcomes + uint256 bobHdBalanceAfter = hdToken.balanceOf(bob); + uint256 vaultHdBalanceAfter = hdToken.balanceOf(address(vault)); + uint256 votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHdBalanceAfter, bobHdBalanceBefore + expectedAllocation, "Bob HD balance incorrect"); + assertEq(vaultHdBalanceAfter, 0, "Vault HD balance incorrect"); + assertEq(votingPowerAfter, 0, "Voting power should be zero after claim"); + assertEq(hdToken.balanceOf(vault.hdTreasury()), treasuryHdBalanceBefore, "Treasury balance incorrect"); + + // Verify grant is deleted + grant = vault.getGrant(bob); + assertEq(grant.allocation, 0, "Grant should be deleted"); + } + + /// @notice Tests migration and full claim after expiration. + function test_migrate_postExpiration_and_claim_postExpiration() external { + uint256 amount = 100e18; + + // Move past expiration + vm.roll(vault.expiration() + 1); + + // Record initial states + uint256 vaultHdBalanceBefore = hdToken.balanceOf(address(vault)); + uint256 bobHdBalanceBefore = hdToken.balanceOf(bob); + + // Alice migrates ELFI to Bob (post-expiration) + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, bob); + vm.stopPrank(); + + // Verify grant configuration (no bonus post-expiration) + uint256 treasuryHdBalanceBefore = hdToken.balanceOf(vault.hdTreasury()); + VestingVaultStorage.Grant memory grant = vault.getGrant(bob); + uint256 expectedBase = amount * 10; // CONVERSION_MULTIPLIER = 10e18 + assertEq(grant.allocation, expectedBase, "Wrong allocation"); + + // Bob claims immediately + vm.startPrank(bob); + vault.claim(); + + // Verify claim outcomes + uint256 bobHdBalanceAfter = hdToken.balanceOf(bob); + uint256 vaultHdBalanceAfter = hdToken.balanceOf(address(vault)); + uint256 votingPowerAfter = vault.queryVotePower(bob, block.number, ""); + assertEq(bobHdBalanceAfter, bobHdBalanceBefore + expectedBase, "Bob HD balance incorrect"); + assertEq(vaultHdBalanceAfter, vaultHdBalanceBefore, "Vault HD balance should not decrease beyond base"); + assertEq(votingPowerAfter, 0, "Voting power should be zero after claim"); + assertEq(hdToken.balanceOf(vault.hdTreasury()), treasuryHdBalanceBefore, "Treasury should receive no bonus post-expiration"); + + // Verify grant is deleted + grant = vault.getGrant(bob); + assertEq(grant.allocation, 0, "Grant should be deleted"); + } + + /// @notice Tests claiming without migrating. This should fail. + function test_claim_without_migrating_failure() external { + // Bob attempts to claim tokens, but there is nothing to claim. + vm.startPrank(bob); + vm.expectRevert(MigrationRewardsVault.NothingToClaim.selector); + vault.claim(); + } + + // ============================== + // Voting Power Tests + // ============================== + + /// @notice Tests voting power after pre-cliff migration. + function test_votingPower_afterPreCliffMigration() external { + uint256 amount = 100e18; + + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, alice); + vm.stopPrank(); + + // Wait past stale blocks + vm.roll(block.number + STALE_BLOCKS + 1); + + // Verify voting power + uint256 votingPower = vault.queryVotePower(alice, block.number - 1, ""); + assertEq(votingPower, amount * 10, "Incorrect voting power pre-cliff"); // Base amount + } + + /// @notice Tests voting power delegation. + function test_votingPower_afterDelegation() external { + uint256 amount = 100e18; + + // Alice migrates and delegates to Bob + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, alice); + vm.expectEmit(true, true, true, true); + emit VoteChange(alice, alice, -int256(amount * 10)); + vm.expectEmit(true, true, true, true); + emit VoteChange(bob, alice, int256(amount * 10)); + vault.delegate(bob); + vm.stopPrank(); + + // Wait past stale blocks + 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 * 10, "Bob should have Alice's voting power"); + } + + /// @notice Tests voting power progression through vesting. + function test_votingPower_throughVesting() external { + uint256 amount = 100e18; + + // Alice migrates pre-cliff + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + vault.migrate(amount, alice); + vm.stopPrank(); + + // Move to cliff + vm.roll(vault.cliff()); + uint256 votingPowerAtCliff = vault.queryVotePower(alice, block.number - 1, ""); + assertEq(votingPowerAtCliff, amount * 10, "Voting power incorrect at cliff"); + + // Move halfway between cliff and expiration + uint256 halfwayBlock = vault.cliff() + (vault.expiration() - vault.cliff()) / 2; + vm.roll(halfwayBlock); + vault.updateVotingPower(alice); + uint256 votingPowerHalfway = vault.queryVotePower(alice, block.number, ""); + uint256 expectedAllocation = (amount * 10 * vault.BONUS_MULTIPLIER()) / vault.ONE(); + uint256 expectedBonusHalf = ((expectedAllocation - (amount * 10)) / 2); + assertEq(votingPowerHalfway, amount * 10 + expectedBonusHalf, "Voting power incorrect halfway"); + + // Move to expiration + vm.roll(vault.expiration()); + vault.updateVotingPower(alice); + uint256 votingPowerAtExpiration = vault.queryVotePower(alice, block.number, ""); + assertEq(votingPowerAtExpiration, expectedAllocation, "Voting power incorrect at expiration"); + } +} diff --git a/test/MigrationVestingVaultTest.t.sol b/test/MigrationVestingVaultTest.t.sol index e77c07a..b536574 100644 --- a/test/MigrationVestingVaultTest.t.sol +++ b/test/MigrationVestingVaultTest.t.sol @@ -18,8 +18,6 @@ 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 = 10; - uint256 internal constant VESTING_DURATION = 90 days; address internal constant ELFI_WHALE = 0x6De73946eab234F1EE61256F10067D713aF0e37A; /// @dev Contract instances @@ -58,16 +56,11 @@ contract MigrationVestingVaultTest is Test { ); // Deploy migration vault - 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, - startBlock, - expiration + STALE_BLOCKS ); vault.initialize(deployer, deployer); @@ -95,6 +88,35 @@ contract MigrationVestingVaultTest is Test { // Migration Tests // ============================== + /// @notice Tests that migration fails with zero migration amount. + function test_migrate_failure_zeroAmount() external { + // Set up Alice to attempt a migration with zero amount + vm.startPrank(alice); + ELFI.approve(address(vault), 1e18); // Approve some amount even though we'll send 0 + + // Expect the specific error for zero amount + vm.expectRevert(MigrationVestingVault.InvalidMigrationAmount.selector); + + // Attempt migration with zero amount + vault.migrate(0, bob); + vm.stopPrank(); + } + + /// @notice Tests that migration fails with zero address destination. + function test_migrate_failure_zeroAddressDestination() external { + // Set up Alice to attempt a migration to the zero address + uint256 amount = 100e18; + vm.startPrank(alice); + ELFI.approve(address(vault), amount); + + // Expect the specific error for zero address destination + vm.expectRevert(MigrationVestingVault.InvalidDestination.selector); + + // Attempt migration to address(0) + vault.migrate(amount, address(0)); + vm.stopPrank(); + } + /// @dev Ensures migration fails when destination already has a grant. function test_migrate_failure_existingGrant() external { // First migration @@ -122,7 +144,7 @@ contract MigrationVestingVaultTest is Test { /// @dev Ensures migration fails when insufficient HD tokens are available function test_migrate_failure_insufficientHDTokens() external { - uint256 excessiveAmount = hdToken.allowance(deployer, address(vault)) / vault.conversionMultiplier() + 1; + uint256 excessiveAmount = hdToken.allowance(deployer, address(vault)) / vault.CONVERSION_MULTIPLIER() + 1; vm.startPrank(alice); ELFI.approve(address(vault), excessiveAmount); @@ -152,7 +174,7 @@ contract MigrationVestingVaultTest is Test { // Ensure that the grant was properly configured. VestingVaultStorage.Grant memory grant = vault.getGrant(bob); - assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); + assertEq(grant.allocation, amount * vault.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.expiration(), "Wrong expiration"); @@ -185,8 +207,8 @@ contract MigrationVestingVaultTest is Test { 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(bobHDBalanceAfter, bobHDBalanceBefore + amount * vault.CONVERSION_MULTIPLIER() / 2, "Bob HD balance not updated"); + assertEq(vaultHDBalanceAfter, vaultHDBalanceBefore - amount * vault.CONVERSION_MULTIPLIER() / 2, "Bob HD balance not updated"); assertEq(votingPowerAfter, votingPowerBefore / 2, "Voting power not updated"); // The other half of the three months passes. @@ -199,7 +221,7 @@ contract MigrationVestingVaultTest is Test { 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(bobHDBalanceAfter, amount * vault.CONVERSION_MULTIPLIER(), "Bob HD balance not updated"); assertEq(vaultHDBalanceAfter, 0, "Bob HD balance not updated"); assertEq(votingPowerAfter, 0, "Voting power not updated"); } @@ -226,7 +248,7 @@ contract MigrationVestingVaultTest is Test { vault.migrate(amount, bob); vm.stopPrank(); VestingVaultStorage.Grant memory grant = vault.getGrant(bob); - assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); + assertEq(grant.allocation, amount * vault.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.expiration(), "Wrong expiration"); @@ -256,8 +278,8 @@ contract MigrationVestingVaultTest is Test { 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(bobHDBalanceAfter, bobHDBalanceBefore + amount * vault.CONVERSION_MULTIPLIER() / 2, "Bob HD balance not updated"); + assertEq(vaultHDBalanceAfter, vaultHDBalanceBefore - amount * vault.CONVERSION_MULTIPLIER() / 2, "Bob HD balance not updated"); assertEq(votingPowerAfter, votingPowerBefore / 2, "Voting power not updated"); // The other half of the three months passes. @@ -270,7 +292,7 @@ contract MigrationVestingVaultTest is Test { 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(bobHDBalanceAfter, amount * vault.CONVERSION_MULTIPLIER(), "Bob HD balance not updated"); assertEq(vaultHDBalanceAfter, 0, "Bob HD balance not updated"); assertEq(votingPowerAfter, 0, "Voting power not updated"); } @@ -296,7 +318,7 @@ contract MigrationVestingVaultTest is Test { vault.migrate(amount, bob); vm.stopPrank(); VestingVaultStorage.Grant memory grant = vault.getGrant(bob); - assertEq(grant.allocation, amount * CONVERSION_MULTIPLIER, "Wrong allocation"); + assertEq(grant.allocation, amount * vault.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.expiration(), "Wrong expiration"); @@ -325,8 +347,8 @@ contract MigrationVestingVaultTest is Test { 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(bobHDBalanceAfter, bobHDBalanceBefore + amount * vault.CONVERSION_MULTIPLIER(), "Bob HD balance not updated"); + assertEq(vaultHDBalanceAfter, vaultHDBalanceBefore - amount * vault.CONVERSION_MULTIPLIER(), "Bob HD balance not updated"); assertEq(votingPowerAfter, 0, "Voting power not updated"); } @@ -350,7 +372,7 @@ contract MigrationVestingVaultTest is Test { uint256 votingPower = vault.queryVotePower(alice, block.number - 1, ""); assertEq( votingPower, - amount * CONVERSION_MULTIPLIER, + amount * vault.CONVERSION_MULTIPLIER(), "Incorrect voting power" ); } @@ -366,9 +388,9 @@ contract MigrationVestingVaultTest is Test { // Delegate to bob vm.expectEmit(true, true, true, true); - emit VoteChange(alice, alice, -int256(uint256(amount * CONVERSION_MULTIPLIER))); + emit VoteChange(alice, alice, -int256(uint256(amount * vault.CONVERSION_MULTIPLIER()))); vm.expectEmit(true, true, true, true); - emit VoteChange(bob, alice, int256(uint256(amount * CONVERSION_MULTIPLIER))); + emit VoteChange(bob, alice, int256(uint256(amount * vault.CONVERSION_MULTIPLIER()))); vault.delegate(bob); vm.stopPrank(); @@ -382,7 +404,7 @@ contract MigrationVestingVaultTest is Test { assertEq(aliceVotingPower, 0, "Alice should have no voting power"); assertEq( bobVotingPower, - amount * CONVERSION_MULTIPLIER, + amount * vault.CONVERSION_MULTIPLIER(), "Bob should have Alice's voting power" ); } @@ -404,7 +426,7 @@ contract MigrationVestingVaultTest is Test { uint256 votingPower = vault.queryVotePower(alice, block.number - 1, ""); assertEq( votingPower, - amount * CONVERSION_MULTIPLIER, + amount * vault.CONVERSION_MULTIPLIER(), "Voting power should remain constant" ); }