Skip to content

Commit

Permalink
feat: delegation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Grimal committed Oct 11, 2024
1 parent 335406f commit f8d4fa5
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 18 deletions.
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 {IERC5805} from
"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC5805.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,
IERC5805
{
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 VotesStorageLocation = 0xe96d6a46d8feefe49b223986c94a74a56f4e1500280e36600ec085ab28160200;

function _getDeleguatesStorage() private pure returns (VotesStorage storage $) {
assembly {
$.slot := VotesStorageLocation
}
}

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

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

/**
* @dev Returns the delegate that `account` has chosen.
*/
function delegates(address account) public view virtual returns (address) {
VotesStorage storage $ = _getVotesStorage();
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 VotesExpiredSignature(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 {
VotesStorage storage $ = _getVotesStorage();
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 {
VotesStorage storage $ = _getVotesStorage();
if (from == address(0)) {
$._totalCheckpoints += amount;
}
if (to == address(0)) {
$._totalCheckpoints -= 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 {
VotesStorage storage $ = _getVotesStorage();
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);
}
}
21 changes: 10 additions & 11 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 @@ -55,9 +54,9 @@ contract MorphoToken is ERC20VotesUpgradeable, ERC20PermitUpgradeable, Ownable2S
internal
override(ERC20Upgradeable, ERC20VotesUpgradeable)
{
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

0 comments on commit f8d4fa5

Please sign in to comment.