From f8d4fa5ad2a89578acde2a21b347496c64f4556f Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:24:36 +0200 Subject: [PATCH] feat: delegation logic --- .../DelegatesUpgradeable.sol | 144 ++++++++++++++++++ .../ERC20DelegatesUpgradeable.sol | 67 ++++++++ src/MorphoToken.sol | 21 ++- src/Wrapper.sol | 14 +- 4 files changed, 228 insertions(+), 18 deletions(-) create mode 100644 src/DelegatesContracts/DelegatesUpgradeable.sol create mode 100644 src/DelegatesContracts/ERC20DelegatesUpgradeable.sol diff --git a/src/DelegatesContracts/DelegatesUpgradeable.sol b/src/DelegatesContracts/DelegatesUpgradeable.sol new file mode 100644 index 0000000..0c68645 --- /dev/null +++ b/src/DelegatesContracts/DelegatesUpgradeable.sol @@ -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); +} diff --git a/src/DelegatesContracts/ERC20DelegatesUpgradeable.sol b/src/DelegatesContracts/ERC20DelegatesUpgradeable.sol new file mode 100644 index 0000000..e0d7078 --- /dev/null +++ b/src/DelegatesContracts/ERC20DelegatesUpgradeable.sol @@ -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); + } +} diff --git a/src/MorphoToken.sol b/src/MorphoToken.sol index d5e024f..1d99718 100644 --- a/src/MorphoToken.sol +++ b/src/MorphoToken.sol @@ -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. @@ -25,10 +24,10 @@ 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 */ @@ -36,7 +35,7 @@ contract MorphoToken is ERC20VotesUpgradeable, ERC20PermitUpgradeable, Ownable2S 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); @@ -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 {} } diff --git a/src/Wrapper.sol b/src/Wrapper.sol index b409003..886fbb6 100644 --- a/src/Wrapper.sol +++ b/src/Wrapper.sol @@ -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; } @@ -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);