Skip to content

Commit

Permalink
Merge pull request #2 from aragon/feat/segmented-escrow
Browse files Browse the repository at this point in the history
Feat/segmented escrow
  • Loading branch information
jordaniza authored Sep 18, 2024
2 parents b9ac673 + 842a082 commit 98912bf
Show file tree
Hide file tree
Showing 21 changed files with 462 additions and 221 deletions.
121 changes: 121 additions & 0 deletions src/escrow/increasing/Lock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {ILock} from "@escrow-interfaces/ILock.sol";
import {ERC721EnumerableUpgradeable as ERC721Enumerable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {DaoAuthorizableUpgradeable as DaoAuthorizable} from "@aragon/osx/core/plugin/dao-authorizable/DaoAuthorizableUpgradeable.sol";
import {IDAO} from "@aragon/osx/core/dao/IDAO.sol";

/// @title NFT representation of an escrow locking mechanism
contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable {
/// @dev enables transfers without whitelisting
address public constant WHITELIST_ANY_ADDRESS =
address(uint160(uint256(keccak256("WHITELIST_ANY_ADDRESS"))));

/// @notice role to upgrade this contract
bytes32 public constant LOCK_ADMIN_ROLE = keccak256("LOCK_ADMIN");

/// @notice Address of the escrow contract that holds underyling assets
address public escrow;

/// @notice Whitelisted contracts that are allowed to transfer
mapping(address => bool) public whitelisted;

/*//////////////////////////////////////////////////////////////
Modifiers
//////////////////////////////////////////////////////////////*/

modifier onlyEscrow() {
if (msg.sender != escrow) revert OnlyEscrow();
_;
}

/*//////////////////////////////////////////////////////////////
ERC165
//////////////////////////////////////////////////////////////*/

function supportsInterface(
bytes4 _interfaceId
) public view override(ERC721Enumerable) returns (bool) {
return super.supportsInterface(_interfaceId);
}

/*//////////////////////////////////////////////////////////////
Initializer
//////////////////////////////////////////////////////////////*/

constructor() {
_disableInitializers();
}

function initialize(
address _escrow,
string memory _name,
string memory _symbol,
address _dao
) external initializer {
__ERC721_init(_name, _symbol);
__DaoAuthorizableUpgradeable_init(IDAO(_dao));
escrow = _escrow;

// allow sending nfts to the escrow
whitelisted[escrow] = true;
emit WhitelistSet(address(this), true);
}

/*//////////////////////////////////////////////////////////////
Transfers
//////////////////////////////////////////////////////////////*/

/// @notice Transfers disabled by default, only whitelisted addresses can receive transfers
function setWhitelisted(address _account, bool _isWhitelisted) external auth(LOCK_ADMIN_ROLE) {
whitelisted[_account] = _isWhitelisted;
emit WhitelistSet(_account, _isWhitelisted);
}

/// @notice Enable transfers to any address without whitelisting
function enableTransfers() external auth(LOCK_ADMIN_ROLE) {
whitelisted[WHITELIST_ANY_ADDRESS] = true;
emit WhitelistSet(WHITELIST_ANY_ADDRESS, true);
}

/// @dev Override the transfer to check if the recipient is whitelisted
/// This avoids needing to check for mint/burn but is less idomatic than beforeTokenTransfer
function _transfer(address _from, address _to, uint256 _tokenId) internal override {
if (whitelisted[WHITELIST_ANY_ADDRESS] || whitelisted[_to]) {
super._transfer(_from, _to, _tokenId);
} else revert NotWhitelisted();
}

/*//////////////////////////////////////////////////////////////
NFT Functions
//////////////////////////////////////////////////////////////*/

function isApprovedOrOwner(address _spender, uint256 _tokenId) external view returns (bool) {
return _isApprovedOrOwner(_spender, _tokenId);
}

/// @notice Minting and burning functions that can only be called by the escrow contract
function mint(address _to, uint256 _tokenId) external onlyEscrow {
_mint(_to, _tokenId);
}

/// @notice Minting and burning functions that can only be called by the escrow contract
function burn(uint256 _tokenId) external onlyEscrow {
_burn(_tokenId);
}

/*//////////////////////////////////////////////////////////////
UUPS Upgrade
//////////////////////////////////////////////////////////////*/

/// @notice Returns the address of the implementation contract in the [proxy storage slot](https://eips.ethereum.org/EIPS/eip-1967) slot the [UUPS proxy](https://eips.ethereum.org/EIPS/eip-1822) is pointing to.
/// @return The address of the implementation contract.
function implementation() public view returns (address) {
return _getImplementation();
}

/// @notice Internal method authorizing the upgrade of the contract via the [upgradeability mechanism for UUPS proxies](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable) (see [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822)).
function _authorizeUpgrade(address) internal virtual override auth(LOCK_ADMIN_ROLE) {}
}
100 changes: 23 additions & 77 deletions src/escrow/increasing/VotingEscrowIncreasing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ pragma solidity ^0.8.17;
// token interfaces
import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {IERC20MetadataUpgradeable as IERC20Metadata} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";
import {ERC721Upgradeable as ERC721} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {ERC721EnumerableUpgradeable as ERC721Enumerable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {IERC721EnumerableMintableBurnable as IERC721EMB} from "./interfaces/IERC721EMB.sol";

// veGovernance
import {IDAO} from "@aragon/osx/core/dao/IDAO.sol";
import {ISimpleGaugeVoter} from "@voting/ISimpleGaugeVoter.sol";
import {IClockUser, IClock} from "@clock/IClock.sol";
import {IClock} from "@clock/IClock.sol";
import {IEscrowCurveIncreasing as IEscrowCurve} from "./interfaces/IEscrowCurveIncreasing.sol";
import {IExitQueue} from "./interfaces/IExitQueue.sol";
import {IVotingEscrowIncreasing as IVotingEscrow, ILockedBalanceIncreasing, IVotingEscrowCore, IDynamicVoter} from "./interfaces/IVotingEscrowIncreasing.sol";
import {IVotingEscrowIncreasing as IVotingEscrow} from "./interfaces/IVotingEscrowIncreasing.sol";

// libraries
import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
Expand All @@ -27,15 +26,12 @@ import {DaoAuthorizableUpgradeable as DaoAuthorizable} from "@aragon/osx/core/pl

contract VotingEscrow is
IVotingEscrow,
IClockUser,
ReentrancyGuard,
Pausable,
DaoAuthorizable,
ERC721Enumerable,
UUPSUpgradeable
{
using SafeERC20 for IERC20;
using SafeCast for uint256;

/// @notice Role required to manage the Escrow curve, this typically will be the DAO
bytes32 public constant ESCROW_ADMIN_ROLE = keccak256("ESCROW_ADMIN");
Expand All @@ -46,10 +42,6 @@ contract VotingEscrow is
/// @notice Role required to withdraw underlying tokens from the contract
bytes32 public constant SWEEPER_ROLE = keccak256("SWEEPER");

/// @dev enables transfers without whitelisting
address public constant WHITELIST_ANY_ADDRESS =
address(uint160(uint256(keccak256("WHITELIST_ANY_ADDRESS"))));

/*//////////////////////////////////////////////////////////////
NFT Data
//////////////////////////////////////////////////////////////*/
Expand All @@ -60,6 +52,9 @@ contract VotingEscrow is
/// @notice Total supply of underlying tokens deposited in the contract
uint256 public totalLocked;

/// @dev tracks the locked balance of each NFT
mapping(uint256 => LockedBalance) private _locked;

/*//////////////////////////////////////////////////////////////
Helper Contracts
//////////////////////////////////////////////////////////////*/
Expand All @@ -80,25 +75,8 @@ contract VotingEscrow is
/// @notice Address of the clock contract that manages epoch and voting periods
address public clock;

/*//////////////////////////////////////////////////////////////
Mappings
//////////////////////////////////////////////////////////////*/

/// @notice Whitelisted contracts that are allowed to transfer
mapping(address => bool) public whitelisted;

/// @dev tracks the locked balance of each NFT
mapping(uint256 => LockedBalance) internal _locked;

/*//////////////////////////////////////////////////////////////
ERC165
//////////////////////////////////////////////////////////////*/

function supportsInterface(
bytes4 _interfaceId
) public view override(ERC721Enumerable) returns (bool) {
return super.supportsInterface(_interfaceId);
}
/// @notice Address of the NFT contract that is the lock
address public lockNFT;

/*//////////////////////////////////////////////////////////////
Initialization
Expand All @@ -108,45 +86,20 @@ contract VotingEscrow is
_disableInitializers();
}

function initialize(
address _token,
address _dao,
string memory _name,
string memory _symbol,
address _clock
) external initializer {
function initialize(address _token, address _dao, address _clock) external initializer {
__DaoAuthorizableUpgradeable_init(IDAO(_dao));
__ReentrancyGuard_init();
__Pausable_init();
__ERC721_init(_name, _symbol);

if (IERC20Metadata(_token).decimals() != 18) revert MustBe18Decimals();
token = _token;
clock = _clock;

// allow sending tokens to this contract
whitelisted[address(this)] = true;
emit WhitelistSet(address(this), true);
}

/*//////////////////////////////////////////////////////////////
Admin Setters
//////////////////////////////////////////////////////////////*/

/// @notice Transfers disabled by default, only whitelisted addresses can receive transfers
function setWhitelisted(
address _account,
bool _isWhitelisted
) external auth(ESCROW_ADMIN_ROLE) {
whitelisted[_account] = _isWhitelisted;
emit WhitelistSet(_account, _isWhitelisted);
}

function enableTransfers() external auth(ESCROW_ADMIN_ROLE) {
whitelisted[WHITELIST_ANY_ADDRESS] = true;
emit WhitelistSet(WHITELIST_ANY_ADDRESS, true);
}

/// @notice Sets the curve contract that calculates the voting power
function setCurve(address _curve) external auth(ESCROW_ADMIN_ROLE) {
curve = _curve;
Expand All @@ -167,6 +120,10 @@ contract VotingEscrow is
clock = _clock;
}

function setLockNFT(address _nft) external auth(ESCROW_ADMIN_ROLE) {
lockNFT = _nft;
}

function pause() external auth(PAUSER_ROLE) {
_pause();
}
Expand All @@ -180,17 +137,18 @@ contract VotingEscrow is
//////////////////////////////////////////////////////////////*/

function isApprovedOrOwner(address _spender, uint256 _tokenId) external view returns (bool) {
return _isApprovedOrOwner(_spender, _tokenId);
return IERC721EMB(lockNFT).isApprovedOrOwner(_spender, _tokenId);
}

/// @notice Fetch all NFTs owned by an address by leveraging the ERC721Enumerable interface
/// @param _owner Address to query
/// @return tokenIds Array of token IDs owned by the address
function ownedTokens(address _owner) public view returns (uint256[] memory tokenIds) {
uint256 balance = balanceOf(_owner);
IERC721EMB enumerable = IERC721EMB(lockNFT);
uint256 balance = enumerable.balanceOf(_owner);
uint256[] memory tokens = new uint256[](balance);
for (uint256 i = 0; i < balance; i++) {
tokens[i] = tokenOfOwnerByIndex(_owner, i);
tokens[i] = enumerable.tokenOfOwnerByIndex(_owner, i);
}
return tokens;
}
Expand Down Expand Up @@ -244,18 +202,6 @@ contract VotingEscrow is
return ISimpleGaugeVoter(voter).isVoting(_tokenId);
}

/*//////////////////////////////////////////////////////////////
ERC721 LOGIC
//////////////////////////////////////////////////////////////*/

/// @dev Override the transfer to check if the recipient is whitelisted
/// This avoids needing to check for mint/burn but is less idomatic than beforeTokenTransfer
function _transfer(address _from, address _to, uint256 _tokenId) internal override {
if (whitelisted[WHITELIST_ANY_ADDRESS] || whitelisted[_to]) {
super._transfer(_from, _to, _tokenId);
} else revert NotWhitelisted();
}

/*//////////////////////////////////////////////////////////////
ESCROW LOGIC
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -283,7 +229,7 @@ contract VotingEscrow is

// increment the total locked supply and get the new tokenId
totalLocked += _value;
uint256 newTokenId = totalSupply() + 1;
uint256 newTokenId = IERC721EMB(lockNFT).totalSupply() + 1;

// write the lock and checkpoint the voting power
LockedBalance memory lock = LockedBalance(_value, startTime);
Expand All @@ -296,7 +242,7 @@ contract VotingEscrow is
IERC20(token).safeTransferFrom(_msgSender(), address(this), _value);

// mint the NFT to complete the deposit
_mint(_to, newTokenId);
IERC721EMB(lockNFT).mint(_to, newTokenId);
emit Deposit(_to, newTokenId, startTime, _value, totalLocked);

return newTokenId;
Expand Down Expand Up @@ -339,13 +285,13 @@ contract VotingEscrow is
function beginWithdrawal(uint256 _tokenId) public nonReentrant whenNotPaused {
// can't exit if you have votes pending
if (isVoting(_tokenId)) revert CannotExit();
address owner = _ownerOf(_tokenId);
address owner = IERC721EMB(lockNFT).ownerOf(_tokenId);

// we can remove the user's voting power as it's no longer locked
_checkpointClear(_tokenId);

// transfer NFT to the queue and queue the exit
_transfer(_msgSender(), address(this), _tokenId);
// transfer NFT to this and queue the exit
IERC721EMB(lockNFT).transferFrom(_msgSender(), address(this), _tokenId);
IExitQueue(queue).queueExit(_tokenId, owner);
}

Expand Down Expand Up @@ -374,7 +320,7 @@ contract VotingEscrow is
totalLocked -= value;

// Burn the NFT and transfer the tokens to the user
_burn(_tokenId);
IERC721EMB(lockNFT).burn(_tokenId);
IERC20(token).safeTransfer(sender, value - fee);

emit Withdraw(sender, _tokenId, value - fee, block.timestamp, totalLocked);
Expand Down
12 changes: 12 additions & 0 deletions src/escrow/increasing/interfaces/IERC721EMB.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";

interface IERC721EnumerableMintableBurnable is IERC721Enumerable {
function mint(address to, uint256 tokenId) external;

function burn(uint256 tokenId) external;

function isApprovedOrOwner(address spender, uint256 tokenId) external view returns (bool);
}
28 changes: 28 additions & 0 deletions src/escrow/increasing/interfaces/ILock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/*///////////////////////////////////////////////////////////////
WHITELIST
//////////////////////////////////////////////////////////////*/
interface IWhitelistEvents {
event WhitelistSet(address indexed account, bool status);
}

interface IWhitelistErrors {
error NotWhitelisted();
}

interface IWhitelist is IWhitelistEvents, IWhitelistErrors {
/// @notice Set whitelist status for an address
function setWhitelisted(address addr, bool isWhitelisted) external;

/// @notice Check if an address is whitelisted
function whitelisted(address addr) external view returns (bool);
}

interface ILock is IWhitelist {
error OnlyEscrow();

/// @notice Address of the escrow contract that holds underyling assets
function escrow() external view returns (address);
}
Loading

0 comments on commit 98912bf

Please sign in to comment.