Skip to content

Commit

Permalink
added sweepNFT to escrow contract and more robust tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jordaniza committed Sep 25, 2024
1 parent 98912bf commit 0daab87
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/escrow/increasing/Lock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ contract Lock is ILock, ERC721Enumerable, UUPSUpgradeable, DaoAuthorizable {

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

/*//////////////////////////////////////////////////////////////
Expand Down
15 changes: 15 additions & 0 deletions src/escrow/increasing/VotingEscrowIncreasing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,21 @@ contract VotingEscrow is
emit Sweep(_msgSender(), excess);
}

/// @notice the sweeper can send NFTs mistakenly sent to the contract to a designated address
/// @param _tokenId the tokenId to sweep - must be currently in this contract
/// @param _to the address to send the NFT to - must be a whitelisted address for transfers
/// @dev Cannot sweep NFTs that are in the exit queue for obvious reasons
function sweepNFT(uint256 _tokenId, address _to) external nonReentrant auth(SWEEPER_ROLE) {
// if the token id is not in the contract, revert
if (IERC721EMB(lockNFT).ownerOf(_tokenId) != address(this)) revert NothingToSweep();

// if the token id is in the queue, we cannot sweep it
if (IExitQueue(queue).ticketHolder(_tokenId) != address(0)) revert CannotExit();

IERC721EMB(lockNFT).transferFrom(address(this), _to, _tokenId);
emit SweepNFT(_to, _tokenId);
}

/*///////////////////////////////////////////////////////////////
UUPS Upgrade
//////////////////////////////////////////////////////////////*/
Expand Down
3 changes: 3 additions & 0 deletions src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ interface IWithdrawalQueue is IWithdrawalQueueErrors, IWithdrawalQueueEvents {

interface ISweeperEvents {
event Sweep(address indexed to, uint256 amount);
event SweepNFT(address indexed to, uint256 tokenId);
}

interface ISweeperErrors {
Expand All @@ -112,6 +113,8 @@ interface ISweeperErrors {
interface ISweeper is ISweeperEvents, ISweeperErrors {
/// @notice sweeps excess tokens from the contract to a designated address
function sweep() external;

function sweepNFT(uint256 _tokenId, address _to) external;
}

/*///////////////////////////////////////////////////////////////
Expand Down
153 changes: 153 additions & 0 deletions test/escrow/escrow/EscrowSweep.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
pragma solidity ^0.8.17;

import {EscrowBase} from "./EscrowBase.sol";

import {console2 as console} from "forge-std/console2.sol";
import {IDAO} from "@aragon/osx/core/dao/IDAO.sol";
import {DAO} from "@aragon/osx/core/dao/DAO.sol";
import {Multisig, MultisigSetup} from "@aragon/multisig/MultisigSetup.sol";

import {ProxyLib} from "@libs/ProxyLib.sol";

import {IEscrowCurveUserStorage} from "@escrow-interfaces/IEscrowCurveIncreasing.sol";
import {VotingEscrow} from "@escrow/VotingEscrowIncreasing.sol";

import {SimpleGaugeVoter, SimpleGaugeVoterSetup} from "src/voting/SimpleGaugeVoterSetup.sol";
import {IGaugeVote} from "src/voting/ISimpleGaugeVoter.sol";

contract TestSweep is EscrowBase, IEscrowCurveUserStorage, IGaugeVote {
function setUp() public override {
super.setUp();

// grant the sweeper role to this contract
dao.grant({
_who: address(this),
_where: address(escrow),
_permissionId: escrow.SWEEPER_ROLE()
});
}

function testCanSweepExcessTokens() public {
token.mint(address(escrow), 1000);

assertEq(token.balanceOf(address(escrow)), 1000);
assertEq(token.balanceOf(address(this)), 0);
assertEq(escrow.totalLocked(), 0);

vm.expectEmit(true, false, false, true);
emit Sweep(address(this), 1000);
escrow.sweep();

assertEq(token.balanceOf(address(escrow)), 0);
assertEq(token.balanceOf(address(this)), 1000);
}

function testCannotSweepFromLocked() public {
address user = address(1);
token.mint(address(user), 1000);

vm.startPrank(user);
{
token.approve(address(escrow), 1000);
escrow.createLock(1000);
}
vm.stopPrank();

assertEq(token.balanceOf(address(escrow)), 1000);
assertEq(token.balanceOf(address(this)), 0);
assertEq(escrow.totalLocked(), 1000);

// first try with no excess tokens
vm.expectRevert(NothingToSweep.selector);
escrow.sweep();

// check that nothing changed
assertEq(token.balanceOf(address(escrow)), 1000);
assertEq(token.balanceOf(address(this)), 0);
assertEq(escrow.totalLocked(), 1000);

// now try with excess tokens
token.mint(address(escrow), 1000);

assertEq(token.balanceOf(address(escrow)), 2000);

escrow.sweep();

assertEq(token.balanceOf(address(escrow)), 1000);
assertEq(token.balanceOf(address(this)), 1000);
assertEq(escrow.totalLocked(), 1000);
}

function testOnlySweeperRole() public {
address notThis = address(1);

bytes memory err = _authErr(notThis, address(escrow), escrow.SWEEPER_ROLE());

vm.prank(notThis);
vm.expectRevert(err);
escrow.sweep();

vm.prank(notThis);
vm.expectRevert(err);
escrow.sweepNFT(1, address(this));
}

function testCannotSweepNFTIfNotInContract() public {
// create a lock
token.mint(address(this), 1000);
token.approve(address(escrow), 1000);
uint tokenId = escrow.createLock(1000);

// try to sweep the NFT -- should fail as it's not in the contract
vm.expectRevert(NothingToSweep.selector);
escrow.sweepNFT(tokenId, address(this));
}

function testCannotSweepNFTIfInQueue() public {
// create lock, enter withdrawal
token.mint(address(this), 1000);
token.approve(address(escrow), 1000);
uint tokenId = escrow.createLock(1000);

// warp to the min lock
vm.warp(1 weeks);

nftLock.approve(address(escrow), tokenId);
escrow.beginWithdrawal(tokenId);

// try to sweep the NFT -- should fail as it's in the queue
vm.expectRevert(CannotExit.selector);
escrow.sweepNFT(tokenId, address(this));
}

function testCannotSweepNFTIfNotWhitelisted() public {
// create the lock and transfer the NFT to the contract
token.mint(address(this), 1000);
token.approve(address(escrow), 1000);
uint tokenId = escrow.createLock(1000);
nftLock.transferFrom(address(this), address(escrow), tokenId);

// try to sweep the NFT -- should fail as this address is not whitelisted
vm.expectRevert(NotWhitelisted.selector);
escrow.sweepNFT(tokenId, address(this));
}

function testCanSweepNFT() public {
// create, transfer, whitelis, sweep
token.mint(address(this), 1000);
token.approve(address(escrow), 1000);
uint tokenId = escrow.createLock(1000);
nftLock.transferFrom(address(this), address(escrow), tokenId);
nftLock.setWhitelisted(address(this), true);

assertEq(nftLock.balanceOf(address(this)), 0);
assertEq(nftLock.balanceOf(address(escrow)), 1);

vm.expectEmit(true, false, false, true);
emit SweepNFT(address(this), tokenId);
escrow.sweepNFT(tokenId, address(this));

assertEq(nftLock.balanceOf(address(this)), 1);
assertEq(nftLock.balanceOf(address(escrow)), 0);
}
}
2 changes: 2 additions & 0 deletions test/escrow/escrow/Lock.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ contract TestLockMintBurn is EscrowBase, IEscrowCurveUserStorage, IGaugeVote {
address _dao,
address _escrow
) public {
vm.expectEmit(true, false, false, true);
emit WhitelistSet(address(_escrow), true);
Lock _nftLock = _deployLock(_escrow, _name, _symbol, _dao);

assertEq(_nftLock.name(), _name);
Expand Down

0 comments on commit 0daab87

Please sign in to comment.