Skip to content

Commit

Permalink
initial poc code from escrowed streams
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrai committed Sep 10, 2024
1 parent c0a805b commit 018a92c
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 2 deletions.
21 changes: 19 additions & 2 deletions packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ReentrancyGuardUpgradeable } from '@openzeppelin/contracts-upgradeable/
import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import { INounsAuctionHouseV2 } from './interfaces/INounsAuctionHouseV2.sol';
import { IStreamEscrow } from './interfaces/IStreamEscrow.sol';
import { INounsToken } from './interfaces/INounsToken.sol';
import { IWETH } from './interfaces/IWETH.sol';

Expand Down Expand Up @@ -69,6 +70,12 @@ contract NounsAuctionHouseV2 is
/// @notice The Nouns price feed state
mapping(uint256 => SettlementState) settlementHistory;

uint16 public immediateTreasuryBps;

uint16 public streamLengthInAuctions;

IStreamEscrow public streamEscrow;

constructor(INounsToken _nouns, address _weth, uint256 _duration) initializer {
nouns = _nouns;
weth = _weth;
Expand Down Expand Up @@ -284,8 +291,18 @@ contract NounsAuctionHouseV2 is
nouns.transferFrom(address(this), _auction.bidder, _auction.nounId);
}

if (_auction.amount > 0) {
_safeTransferETHWithFallback(owner(), _auction.amount);
uint256 amountToSendTreasury = (_auction.amount * immediateTreasuryBps) / 10_000;
uint256 amountToStream = _auction.amount - amountToSendTreasury;

if (amountToSendTreasury > 0) {
_safeTransferETHWithFallback(owner(), amountToSendTreasury);
}

// TODO maybe separate in case there's no winner and no auction.amount?
if (amountToStream > 0) {
streamEscrow.createStreamAndForwardAll{ value: amountToStream }(_auction.nounId, streamLengthInAuctions);
} else {
streamEscrow.forwardAll();
}

SettlementState storage settlementState = settlementHistory[_auction.nounId];
Expand Down
111 changes: 111 additions & 0 deletions packages/nouns-contracts/contracts/StreamEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-3.0

/// @title Stream Escrow

/*********************************
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░██░░░████░░██░░░████░░░ *
* ░░██████░░░████████░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
*********************************/

pragma solidity ^0.8.19;

import { IStreamEscrow } from './interfaces/IStreamEscrow.sol';
import { INounsToken } from './interfaces/INounsToken.sol';

contract StreamEscrow is IStreamEscrow {
struct Stream {
uint256 ethPerAuction;
bool canceled;
uint256 streamEndId;
}

address public daoTreasury;
address public auctionHouse;
INounsToken public nounsToken; // TODO immutable?

uint256 public ethStreamedPerAuction;
uint256 public ethStreamedToDAO;
uint256 public ethWithdrawn;
mapping(uint256 streamEndId => uint256[] streamIds) public streamEndIds;
mapping(uint256 streamId => Stream) streams;
uint256 public auctionsCounter;

constructor(address daoTreasury_, address auctionHouse_, address nounsToken_) {
daoTreasury = daoTreasury_;
auctionHouse = auctionHouse_;
nounsToken = INounsToken(nounsToken_);
}

function createStreamAndForwardAll(uint256 nounId, uint16 streamLengthInAuctions) external payable {
require(msg.sender == auctionHouse, 'only auction house');

// register new stream
uint256 streamEndId = auctionsCounter + streamLengthInAuctions + 1;
streamEndIds[streamEndId].push(nounId);

// TODO: check for rounding issues. probably best to immediately vest the rounded down amount
uint256 ethPerAuction = msg.value / streamLengthInAuctions;
ethStreamedPerAuction += ethPerAuction;
streams[nounId] = Stream({ ethPerAuction: ethPerAuction, canceled: false, streamEndId: streamEndId });

forwardAll();
}

// used for example when there were no bids on a noun
function forwardAll() public {
require(msg.sender == auctionHouse, 'only auction house');

auctionsCounter++;
finishStreams();
ethStreamedToDAO += ethStreamedPerAuction;
}

// TODO add versions with uint256[] nounIds?
function cancelStream(uint256 nounId) external {
// transfer noun to treasury
nounsToken.transferFrom(msg.sender, daoTreasury, nounId);

// cancel stream
require(!streams[nounId].canceled, 'already canceled');
streams[nounId].canceled = true;
ethStreamedPerAuction -= streams[nounId].ethPerAuction;

// calculate how much needs to be refunded
// TODO: assuming lastSeenNounId < streamEndId, but need to handle the other case
uint256 auctionsLeft = streams[nounId].streamEndId - auctionsCounter - 1;
uint256 amountToRefund = streams[nounId].ethPerAuction * auctionsLeft;
(bool sent, ) = msg.sender.call{ value: amountToRefund }('');
require(sent, 'failed to send eth');
}

function withdrawToTreasury(uint256 amount) external {
require(msg.sender == daoTreasury);
require(amount >= (ethStreamedToDAO - ethWithdrawn), 'not enough to withdraw');
ethWithdrawn += amount;
(bool sent, ) = daoTreasury.call{ value: amount }('');
require(sent, 'failed to send eth');
}

function setDAOTreasuryAddress(address newAddress) external {
require(msg.sender == daoTreasury);
daoTreasury = newAddress;
}

function finishStreams() internal {
uint256[] storage endingStreams = streamEndIds[auctionsCounter];
for (uint256 i; i < endingStreams.length; i++) {
uint256 streamId = endingStreams[i];
if (!streams[streamId].canceled) {
ethStreamedPerAuction -= streams[streamId].ethPerAuction;
}
}
}
}
24 changes: 24 additions & 0 deletions packages/nouns-contracts/contracts/interfaces/IStreamEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0

/// @title Interface for Stream Escrow

/*********************************
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░██░░░████░░██░░░████░░░ *
* ░░██████░░░████████░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
*********************************/

pragma solidity ^0.8.19;

interface IStreamEscrow {
function createStreamAndForwardAll(uint256 nounId, uint16 streamLengthInAuctions) external payable;

function forwardAll() external;
}
71 changes: 71 additions & 0 deletions packages/nouns-contracts/test/foundry/StreamEscrow.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import { Test } from 'forge-std/Test.sol';
import { StreamEscrow } from '../../contracts/StreamEscrow.sol';
import { ERC721Mock } from './helpers/ERC721Mock.sol';

contract StreamEscrowTest is Test {
StreamEscrow escrow;
address treasury = makeAddr('treasury');
address auctionHouse = makeAddr('auctionHouse');
ERC721Mock nounsToken = new ERC721Mock();
address user = makeAddr('user');

function setUp() public {
escrow = new StreamEscrow(treasury, auctionHouse, address(nounsToken));

vm.deal(auctionHouse, 1000 ether);
}

function testSingleStream() public {
vm.prank(auctionHouse);
escrow.createStreamAndForwardAll{ value: 10 ether }({ nounId: 1, streamLengthInAuctions: 20 });

// check that one 'tick' has been streamed
assertEq(escrow.ethStreamedToDAO(), 0.5 ether);

for (uint i; i < 3; i++) {
vm.prank(auctionHouse);
escrow.forwardAll();
}

assertEq(escrow.ethStreamedToDAO(), 2 ether);

vm.prank(treasury);
escrow.withdrawToTreasury(2 ether);

// forward past the point of stream ending
for (uint i; i < 20; i++) {
vm.prank(auctionHouse);
escrow.forwardAll();
}

assertEq(escrow.ethStreamedToDAO(), 10 ether);
}

function testCancelStream() public {
nounsToken.mint(user, 1);

vm.prank(auctionHouse);
escrow.createStreamAndForwardAll{ value: 10 ether }({ nounId: 1, streamLengthInAuctions: 20 });

for (uint i; i < 3; i++) {
vm.prank(auctionHouse);
escrow.forwardAll();
}

assertEq(escrow.ethStreamedToDAO(), 2 ether);

vm.prank(user);
nounsToken.approve(address(escrow), 1);
vm.prank(user);
escrow.cancelStream(1);

// make sure moving forward works with canceled streams
for (uint i; i < 20; i++) {
vm.prank(auctionHouse);
escrow.forwardAll();
}
}
}

1 comment on commit 018a92c

@dmrazzy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense l

Please sign in to comment.