Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ out = "out"
libs = ["lib"]

remappings = [
'council/=lib/council/contracts',
'openzeppelin/=lib/openzeppelin-contracts/contracts',
]

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
1 change: 1 addition & 0 deletions lib/council
Submodule council added at 5f7be3
115 changes: 115 additions & 0 deletions src/MigrationVestingVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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 MigrationVestingVault
/// @notice A migration vault that converts ELFI tokens to HD tokens. Migrated
/// 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;

/// @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 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;

/// @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
/// 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 _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
) AbstractVestingVault(_hdToken, _stale) {
hdTreasury = _hdTreasury;
elfiToken = _elfiToken;
conversionMultiplier = _conversionMultiplier;
startBlock = _startBlock;
expiration = _expiration;
}

/// @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];
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 HD token amount to be granted.
uint256 hdAmount = amount * conversionMultiplier;

// Pull the HD tokens from the source.
if (!token.transferFrom(hdTreasury, address(this), hdAmount)) {
Copy link

Choose a reason for hiding this comment

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

what happens to the extra tokens pulled from treasury if someone withdraws early?

Copy link

Choose a reason for hiding this comment

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

hmm actually, they should just continue vesting. sorry, just thinking outloud

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries, thinking out loud is good

revert InsufficientHDTokens();
}

// 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: uint128(startBlock),
expiration: uint128(expiration),
cliff: uint128(startBlock), // vesting starts immediately
latestVotingPower: initialVotingPower,
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)));
}
}
Loading
Loading