Skip to content

Commit 8423f8f

Browse files
authored
Merge pull request #1 from delvtech/jalextowle/migration/migration-vesting-vault
Migration Vesting Vault
2 parents 503b6d4 + c5eb1f0 commit 8423f8f

5 files changed

Lines changed: 534 additions & 0 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "lib/openzeppelin-contracts"]
55
path = lib/openzeppelin-contracts
66
url = https://github.com/OpenZeppelin/openzeppelin-contracts
7+
[submodule "lib/council"]
8+
path = lib/council
9+
url = https://github.com/delvtech/council

foundry.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ out = "out"
44
libs = ["lib"]
55

66
remappings = [
7+
'council/=lib/council/contracts',
78
'openzeppelin/=lib/openzeppelin-contracts/contracts',
89
]
10+
11+
[rpc_endpoints]
12+
mainnet = "${MAINNET_RPC_URL}"

lib/council

Submodule council added at 5f7be33

src/MigrationVestingVault.sol

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity 0.8.24;
3+
4+
import { IERC20 } from "council/interfaces/IERC20.sol";
5+
import { IVotingVault } from "council/interfaces/IVotingVault.sol";
6+
import { History } from "council/libraries/History.sol";
7+
import { VestingVaultStorage } from "council/libraries/VestingVaultStorage.sol";
8+
import { Storage } from "council/libraries/Storage.sol";
9+
import { AbstractVestingVault } from "council/vaults/VestingVault.sol";
10+
11+
/// @title MigrationVestingVault
12+
/// @notice A migration vault that converts ELFI tokens to HD tokens. Migrated
13+
/// tokens are granted with a linear vesting schedule. The grant is
14+
/// created at a destination address provided by the migrator. This
15+
/// contract inherits full voting power tracking from
16+
/// `AbstractVestingVault`.
17+
contract MigrationVestingVault is AbstractVestingVault {
18+
using History for History.HistoricalBalances;
19+
20+
/// @dev Thrown when an existing grant is found.
21+
error ExistingGrantFound();
22+
23+
/// @dev Thrown when ELFI transfers fail.
24+
error ElfiTransferFailed();
25+
26+
/// @dev Thrown when there are insufficient HD tokens.
27+
error InsufficientHDTokens();
28+
29+
/// @dev The HD treasury that is funding this migration contract.
30+
address public immutable hdTreasury;
31+
32+
/// @dev The ELFI token to migrate from.
33+
IERC20 public immutable elfiToken;
34+
35+
/// @dev The conversion rate from ELFI to HD.
36+
uint256 public immutable conversionMultiplier;
37+
38+
/// @dev The global start block at which all grants start vesting.
39+
uint256 public immutable startBlock;
40+
41+
/// @dev The global expiration block at which all grants fully vest.
42+
uint256 public immutable expiration;
43+
44+
/// @notice Constructs the migration vault.
45+
/// @param _hdTreasury The HD treasury that is funding this migration
46+
/// contract.
47+
/// @param _hdToken The ERC20 token to be vested (HD token).
48+
/// @param _elfiToken The ERC20 token to migrate from (ELFI token).
49+
/// @param _stale The stale block lag used in voting power calculations.
50+
/// @param _conversionMultiplier The conversion multiplier from ELFI to HD.
51+
/// @param _startBlock The global start block for all grants.
52+
/// @param _expiration The global expiration block for all grants.
53+
constructor(
54+
address _hdTreasury,
55+
IERC20 _hdToken,
56+
IERC20 _elfiToken,
57+
uint256 _stale,
58+
uint256 _conversionMultiplier,
59+
uint256 _startBlock,
60+
uint256 _expiration
61+
) AbstractVestingVault(_hdToken, _stale) {
62+
hdTreasury = _hdTreasury;
63+
elfiToken = _elfiToken;
64+
conversionMultiplier = _conversionMultiplier;
65+
startBlock = _startBlock;
66+
expiration = _expiration;
67+
}
68+
69+
/// @notice Migrates a specified amount of ELFI tokens into a vesting grant of HD tokens.
70+
/// @dev The caller must have approved this contract for the ELFI token amount.
71+
/// The destination address must not have an existing grant.
72+
/// @param amount The number of tokens to migrate (in ELFI units).
73+
/// @param destination The address at which the vesting grant will be created.
74+
function migrate(uint256 amount, address destination) external {
75+
// Ensure the destination does not already have an active grant.
76+
VestingVaultStorage.Grant storage existingGrant = _grants()[destination];
77+
if (existingGrant.allocation != 0) {
78+
revert ExistingGrantFound();
79+
}
80+
81+
// Transfer ELFI tokens from the caller to this contract.
82+
if (!elfiToken.transferFrom(msg.sender, address(this), amount)) {
83+
revert ElfiTransferFailed();
84+
}
85+
86+
// Calculate the HD token amount to be granted.
87+
uint256 hdAmount = amount * conversionMultiplier;
88+
89+
// Pull the HD tokens from the source.
90+
if (!token.transferFrom(hdTreasury, address(this), hdAmount)) {
91+
revert InsufficientHDTokens();
92+
}
93+
94+
// Calculate the initial voting power using the current unvested multiplier.
95+
Storage.Uint256 memory unvestedMultiplier = _unvestedMultiplier();
96+
uint128 initialVotingPower = uint128((hdAmount * uint128(unvestedMultiplier.data)) / 100);
97+
98+
// Create the grant at the destination address.
99+
_grants()[destination] = VestingVaultStorage.Grant({
100+
allocation: uint128(hdAmount),
101+
withdrawn: 0,
102+
created: uint128(startBlock),
103+
expiration: uint128(expiration),
104+
cliff: uint128(startBlock), // vesting starts immediately
105+
latestVotingPower: initialVotingPower,
106+
delegatee: destination,
107+
range: [uint256(0), uint256(0)]
108+
});
109+
110+
// Update the destination's voting power.
111+
History.HistoricalBalances memory votingPower = History.load("votingPower");
112+
votingPower.push(destination, initialVotingPower);
113+
emit VoteChange(destination, destination, int256(uint256(initialVotingPower)));
114+
}
115+
}

0 commit comments

Comments
 (0)