Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Delegation logic #1

Merged
merged 44 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f8d4fa5
feat: delegation logic
Jean-Grimal Oct 11, 2024
e6c3b57
fix: typo
Jean-Grimal Oct 11, 2024
06234f9
fix: typos and imports
Jean-Grimal Oct 11, 2024
03ab532
fix: imports
Jean-Grimal Oct 11, 2024
f0616f2
refactor: move idelegates to interface folder
MerlinEgalite Oct 12, 2024
e3312db
refactor: remove useless logic
MerlinEgalite Oct 12, 2024
e22f093
refactor: remove delegate contracts folder
MerlinEgalite Oct 12, 2024
1ed972f
fix: compiling
MerlinEgalite Oct 12, 2024
04dc962
refactor: merge files to save lines of code
MerlinEgalite Oct 12, 2024
3a2b459
chore: fmt
MerlinEgalite Oct 12, 2024
59e8249
Merge pull request #2 from morpho-org/perf/delegation
MerlinEgalite Oct 14, 2024
4a5125b
test: add tests
Jean-Grimal Oct 15, 2024
2382b28
test: migration
Jean-Grimal Oct 15, 2024
168fa84
refactor: rename storage vars
MerlinEgalite Oct 16, 2024
4253fab
fix: storage slot
MerlinEgalite Oct 16, 2024
7420d9a
refactor: clean contract
MerlinEgalite Oct 16, 2024
c6c8340
docs: fix typo
MerlinEgalite Oct 16, 2024
bc3be33
refactor: apply suggestions
MerlinEgalite Oct 16, 2024
49667d8
Merge pull request #3 from morpho-org/feat/merge-files-test
MerlinEgalite Oct 16, 2024
702fe4f
Merge pull request #7 from morpho-org/perf/delegation
MerlinEgalite Oct 16, 2024
8179011
Merge branch 'feat/delegation' of github.com:morpho-org/morpho-token-…
Jean-Grimal Oct 16, 2024
a8e27b1
docs: add natspecs + update docs
MerlinEgalite Oct 16, 2024
f6fb0a2
refactor: reorder functions
MerlinEgalite Oct 16, 2024
ee9c4ce
fix: delegateBySig test
Jean-Grimal Oct 16, 2024
263f020
fix: apply suggestions
Jean-Grimal Oct 16, 2024
b6028f9
fix: typo
Jean-Grimal Oct 16, 2024
1d60593
Merge pull request #8 from morpho-org/perf/clean-contracts
MerlinEgalite Oct 16, 2024
452ecea
test: complete coverage
Jean-Grimal Oct 16, 2024
e036e84
docs: missing natspecs
MerlinEgalite Oct 16, 2024
fe09255
feat: add withdraw to function
MerlinEgalite Oct 16, 2024
08a9cb8
fix: solidity versions
Jean-Grimal Oct 17, 2024
b1f3eeb
feat: add .env.example
Jean-Grimal Oct 17, 2024
6e4a861
feat: add README
Jean-Grimal Oct 17, 2024
e5dff0a
Merge pull request #10 from morpho-org/feat/withdraw-to
MerlinEgalite Oct 17, 2024
71f9250
docs: apply suggestions
MerlinEgalite Oct 17, 2024
58fa5a2
Merge pull request #11 from morpho-org/feat/add-readme
MerlinEgalite Oct 17, 2024
c212ed3
fix: merge conflicts
Jean-Grimal Oct 17, 2024
3cbd884
test: withdrawTo
Jean-Grimal Oct 17, 2024
d802c92
fix: remove todo comment
Jean-Grimal Oct 17, 2024
179307a
Merge pull request #6 from morpho-org/test/delegation-tests
Jean-Grimal Oct 17, 2024
8a13849
fix: remove unused function
Jean-Grimal Oct 17, 2024
084f751
Merge pull request #12 from morpho-org/feat/add-total-voting-power-ge…
Jean-Grimal Oct 17, 2024
1dd2c73
fix: env variables
Jean-Grimal Oct 17, 2024
8ec8df8
Merge branch 'main' of github.com:morpho-org/morpho-token-upgradeable…
MerlinEgalite Oct 18, 2024
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
144 changes: 144 additions & 0 deletions src/DelegatesContracts/DelegatesUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IDelegates} from "./IDelegates.sol";
import {ECDSA} from
"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
import {ContextUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol";
import {NoncesUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/NoncesUpgradeable.sol";
import {EIP712Upgradeable} from
"lib/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/EIP712Upgradeable.sol";
import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";

abstract contract DelegatesUpgradeable is
Initializable,
ContextUpgradeable,
EIP712Upgradeable,
NoncesUpgradeable,
IDelegates
{
bytes32 private constant DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

/// @custom:storage-location erc7201:morpho.storage.Delegates
struct DelegatesStorage {
mapping(address account => address) _delegatee;
mapping(address delegatee => uint256) _votingPower;
uint256 _totalVotingPower;
}

// keccak256(abi.encode(uint256(keccak256("morpho.storage.Delegates")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant DelegatesStorageLocation =
0xe96d6a46d8feefe49b223986c94a74a56f4e1500280e36600ec085ab28160200;

function _getDelegatesStorage() private pure returns (DelegatesStorage storage $) {
assembly {
$.slot := DelegatesStorageLocation
}
}

/**
* @dev Returns the current amount of votes that `account` has.
*/
function getVotes(address account) public view virtual returns (uint256) {
DelegatesStorage storage $ = _getDelegatesStorage();
return $._votingPower[account];
}

/**
* @dev Returns the current total supply of votes.
*/
function _getTotalSupply() internal view virtual returns (uint256) {
DelegatesStorage storage $ = _getDelegatesStorage();
return $._totalVotingPower;
}

/**
* @dev Returns the delegate that `account` has chosen.
*/
function delegates(address account) public view virtual returns (address) {
DelegatesStorage storage $ = _getDelegatesStorage();
return $._delegatee[account];
}

/**
* @dev Delegates votes from the sender to `delegatee`.
*/
function delegate(address delegatee) public virtual {
address account = _msgSender();
_delegate(account, delegatee);
}

/**
* @dev Delegates votes from signer to `delegatee`.
*/
function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)
public
virtual
{
if (block.timestamp > expiry) {
revert DelegatesExpiredSignature(expiry);
}
address signer = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry))), v, r, s
);
_useCheckedNonce(signer, nonce);
_delegate(signer, delegatee);
}

/**
* @dev Delegate all of `account`'s voting units to `delegatee`.
*
* Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}.
*/
function _delegate(address account, address delegatee) internal virtual {
DelegatesStorage storage $ = _getDelegatesStorage();
address oldDelegate = delegates(account);
$._delegatee[account] = delegatee;

emit DelegateChanged(account, oldDelegate, delegatee);
_moveDelegateVotes(oldDelegate, delegatee, _getVotingUnits(account));
}

/**
* @dev Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to`
* should be zero. Total supply of voting units will be adjusted with mints and burns.
*/
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual {
DelegatesStorage storage $ = _getDelegatesStorage();
if (from == address(0)) {
$._totalVotingPower += amount;
}
if (to == address(0)) {
$._totalVotingPower -= amount;
}
_moveDelegateVotes(delegates(from), delegates(to), amount);
}

/**
* @dev Moves delegated votes from one delegate to another.
*/
function _moveDelegateVotes(address from, address to, uint256 amount) private {
DelegatesStorage storage $ = _getDelegatesStorage();
if (from != to && amount > 0) {
if (from != address(0)) {
uint256 oldValue = $._votingPower[from];
uint256 newValue = oldValue - amount;
$._votingPower[from] = newValue;
emit DelegateVotesChanged(from, oldValue, newValue);
}
if (to != address(0)) {
uint256 oldValue = $._votingPower[to];
uint256 newValue = oldValue + amount;
$._votingPower[to] = newValue;
emit DelegateVotesChanged(to, oldValue, newValue);
}
}
}

/**
* @dev Must return the voting units held by an account.
*/
function _getVotingUnits(address) internal view virtual returns (uint256);
}
67 changes: 67 additions & 0 deletions src/DelegatesContracts/ERC20DelegatesUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {ERC20Upgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol";
import {DelegatesUpgradeable} from "./DelegatesUpgradeable.sol";
import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
/**
* @dev Extension of ERC20 to support token delegation. |
*
* This extension keeps track of each account's vote power. Vote power can be delegated eithe by calling the
* {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting power can be
* queried through the public accessor {getVotes}.
*
* By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it
* requires users to delegate to themselves in order to activate their voting power.
*/

abstract contract ERC20DelegatesUpgradeable is Initializable, ERC20Upgradeable, DelegatesUpgradeable {
/**
* @dev Total supply cap has been exceeded, introducing a risk of votes overflowing.
*/
error ERC20ExceededSafeSupply(uint256 increasedSupply, uint256 cap);

function __ERC20Delegates_init() internal onlyInitializing {}

function __ERC20Delegates_init_unchained() internal onlyInitializing {}
/**
* @dev Maximum token supply. Defaults to `type(uint208).max` (2^208^ - 1).
*
* This maximum is enforced in {_update}. Increasing this value will not remove the underlying limitation, and
* will cause {_update} to fail because of a math overflow in {_transferVotingUnits}. An override could be
* used to further restrict the total supply (to a lower value) if additional logic requires it. When resolving
* override conflicts on this function, the minimum should be returned.
*/

function _maxSupply() internal view virtual returns (uint256) {
return type(uint256).max;
}

/**
* @dev Move voting power when tokens are transferred.
*
* Emits a {IVotes-DelegateVotesChanged} event.
*/
function _update(address from, address to, uint256 value) internal virtual override {
super._update(from, to, value);
if (from == address(0)) {
uint256 supply = totalSupply();
uint256 cap = _maxSupply();
if (supply > cap) {
revert ERC20ExceededSafeSupply(supply, cap);
}
}
_transferVotingUnits(from, to, value);
}

/**
* @dev Returns the voting units of an `account`.
*
* WARNING: Overriding this function may compromise the internal vote accounting.
* `ERC20Delegates` assumes tokens map to voting units 1:1 and this is not easy to change.
*/
function _getVotingUnits(address account) internal view virtual override returns (uint256) {
return balanceOf(account);
}
}
39 changes: 39 additions & 0 deletions src/DelegatesContracts/IDelegates.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IDelegates {
/**
* @dev The signature used has expired.
*/
error DelegatesExpiredSignature(uint256 expiry);

/**
* @dev Emitted when an account changes their delegate.
*/
event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);

/**
* @dev Emitted when a token transfer or delegate change results in changes to a delegate's number of voting units.
*/
event DelegateVotesChanged(address indexed delegate, uint256 previousVotes, uint256 newVotes);

/**
* @dev Returns the current amount of votes that `account` has.
*/
function getVotes(address account) external view returns (uint256);

/**
* @dev Returns the delegate that `account` has chosen.
*/
function delegates(address account) external view returns (address);

/**
* @dev Delegates votes from the sender to `delegatee`.
*/
function delegate(address delegatee) external;

/**
* @dev Delegates votes from signer to `delegatee`.
*/
function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external;
}
23 changes: 11 additions & 12 deletions src/MorphoToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ pragma solidity ^0.8.13;
import {ERC20Upgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol";
import {Ownable2StepUpgradeable} from
"lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol";
import {ERC20VotesUpgradeable} from
"lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import {ERC20DelegatesUpgradeable} from "./DelegatesContracts/ERC20DelegatesUpgradeable.sol";
import {
ERC20PermitUpgradeable,
NoncesUpgradeable
} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
import { UUPSUpgradeable } from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
import {UUPSUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";

// TODO:
// - add natspecs
// - add events?
// - add error messages
contract MorphoToken is ERC20VotesUpgradeable, ERC20PermitUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable {
contract MorphoToken is ERC20DelegatesUpgradeable, ERC20PermitUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable {
/* CONSTANTS */

/// @dev the name of the token.
Expand All @@ -25,18 +24,18 @@ contract MorphoToken is ERC20VotesUpgradeable, ERC20PermitUpgradeable, Ownable2S
/// @dev the symbol of the token.
string internal constant SYMBOL = "MORPHO";

/* ERRORS */
/* ERRORS */

/// @notice Reverts if the address is the zero address.
error ZeroAddress();
/// @notice Reverts if the address is the zero address.
error ZeroAddress();

/* PUBLIC */

function initialize(address dao, address wrapper) public initializer {
require(dao != address(0), ZeroAddress());
require(wrapper != address(0), ZeroAddress());

ERC20VotesUpgradeable.__ERC20Votes_init();
ERC20DelegatesUpgradeable.__ERC20Delegates_init();
ERC20Upgradeable.__ERC20_init(NAME, SYMBOL);
Ownable2StepUpgradeable.__Ownable2Step_init();
ERC20PermitUpgradeable.__ERC20Permit_init(NAME);
Expand All @@ -53,11 +52,11 @@ contract MorphoToken is ERC20VotesUpgradeable, ERC20PermitUpgradeable, Ownable2S

function _update(address from, address to, uint256 value)
internal
override(ERC20Upgradeable, ERC20VotesUpgradeable)
override(ERC20Upgradeable, ERC20DelegatesUpgradeable)
{
ERC20VotesUpgradeable._update(from, to, value);
ERC20DelegatesUpgradeable._update(from, to, value);
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal override onlyOwner {}
/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal override onlyOwner {}
}
14 changes: 7 additions & 7 deletions src/Wrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ contract Wrapper {
/// @dev The address of the new morpho token.
address public immutable NEW_MORPHO;

/* ERRORS */
/* ERRORS */

/// @notice Reverts if the address is the zero address.
error ZeroAddress();
/// @notice Reverts if the address is the zero address.
error ZeroAddress();

/// @notice Reverts if the address is the contract address.
error SelfAddress();
/// @notice Reverts if the address is the contract address.
error SelfAddress();

/* CONSTRUCTOR */

/// @dev morphoToken address can be precomputed using create2.
constructor(address morphoToken) {
require(morphoToken != address(0), ZeroAddress());
require(morphoToken != address(0), ZeroAddress());

NEW_MORPHO = morphoToken;
}
Expand All @@ -39,7 +39,7 @@ contract Wrapper {

/// @dev Compliant to `ERC20Wrapper` contract from OZ for convenience.
function depositFor(address account, uint256 amount) public returns (bool) {
require(account != address(0), ZeroAddress());
require(account != address(0), ZeroAddress());
require(account != address(this), SelfAddress());

IERC20(LEGACY_MORPHO).transferFrom(msg.sender, address(this), amount);
Expand Down
Loading