-
Notifications
You must be signed in to change notification settings - Fork 434
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial poc code from escrowed streams
- Loading branch information
Showing
4 changed files
with
225 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
24
packages/nouns-contracts/contracts/interfaces/IStreamEscrow.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
018a92c
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense l