From 3ee5280b270b64e4e37bb769401eed03878955d5 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Mon, 30 Oct 2023 11:30:23 +0000 Subject: [PATCH] feat: honeypotdao and deployment script (#5) Signed-off-by: Evaldo Felipe Signed-off-by: Pablo Maldonado Co-authored-by: Evaldo Felipe --- .gitignore | 1 + README.md | 39 ++++++++++++--- script/HoneyPot.s.sol | 49 +++++++++++++++++++ src/HoneyPotDAO.sol | 21 ++++++++ src/mock/ChronicleMedianSourceMock.sol | 29 +++++++++++ test/HoneyPot.sol | 67 +++++++++++++++++++++++++- 6 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 script/HoneyPot.s.sol create mode 100644 src/HoneyPotDAO.sol create mode 100644 src/mock/ChronicleMedianSourceMock.sol diff --git a/.gitignore b/.gitignore index d8a1d07..b33441b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ cache/ out/ +broadcast/ \ No newline at end of file diff --git a/README.md b/README.md index 9457853..a0c69a3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - #

OEV Share HoneyPot Demo

**This repository is a demonstration of the OEV Share system and a HoneyPot mechanism. It showcases how a backrunner can liquidate a position, in this particular case, how a HoneyPot can be emptied given a specific price change.** @@ -12,16 +11,42 @@ The HoneyPot mechanism is a unique setup where funds are kept in a contract that ## Getting Started To test the demo run the following commands: + ``` forge install export RPC_MAINNET=https://mainnet.infura.io/v3/ -forge test` +forge test` ``` ## Contracts Overview -- **HoneyPot**: Represents the honey pot, which can be emptied when a price oracle returns a value different from a pre-defined liquidation price. The honey pot's funds can also be reset by its owner. - -- **HoneyPotOEVShare**: Acts as the oracle which retrieves prices from various sources like Chainlink, Chronicle, and Pyth. - -- **Test Contract**: Sets up the environment, including simulating price changes and testing the mechanisms for creating and emptying the HoneyPot. \ No newline at end of file +- **HoneyPot**: Represents the honey pot, which can be emptied when a price oracle returns a value different from a pre-defined liquidation price. The honey pot's funds can also be reset by its owner. +- **HoneyPotOEVShare**: Acts as the oracle which retrieves prices from various sources like Chainlink, Chronicle, and Pyth. +- **Test Contract**: Sets up the environment, including simulating price changes and testing the mechanisms for creating and emptying the HoneyPot. + +## Deploy the Contracts + +Can be run against a fork with anvil: + +```bash +anvil --fork-url https://mainnet.infura.io/v3/ +``` + +Then: + +```bash + export MNEMONIC="test test test test test test test test test test test junk" + export DEPLOYER_WALLET=$(cast wallet address --mnemonic "$MNEMONIC") + export ETH_RPC_URL="http://127.0.0.1:8545" + + # The following variables can be skipped if you want to use the default values + export CHAINLINK_SOURCE = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" // chosen from https://docs.chain.link/docs/reference-contracts + export PYTH_SOURCE = "0x4305FB66699C3B2702D4d05CF36551390A4c69C6" // chosen from https://pyth.network/markets + export PYTH_PRICE_ID = "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace" // chosen from https://pyth.network/markets + + forge script script/HoneyPot.s.sol \ + --fork-url $ETH_RPC_URL \ + --mnemonics "$MNEMONIC" \ + --sender $DEPLOYER_WALLET \ + --broadcast +``` diff --git a/script/HoneyPot.s.sol b/script/HoneyPot.s.sol new file mode 100644 index 0000000..d05bd65 --- /dev/null +++ b/script/HoneyPot.s.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "forge-std/console2.sol"; +import "forge-std/Script.sol"; + +import {ChronicleMedianSourceMock} from "../src/mock/ChronicleMedianSourceMock.sol"; +import {IMedian} from "oev-contracts/interfaces/chronicle/IMedian.sol"; +import {HoneyPotOEVShare} from "../src/HoneyPotOEVShare.sol"; +import {HoneyPot} from "../src/HoneyPot.sol"; +import {HoneyPotDAO} from "../src/HoneyPotDAO.sol"; +import {IAggregatorV3Source} from "oev-contracts/interfaces/chainlink/IAggregatorV3Source.sol"; + +contract HoneyPotDeploymentScript is Script { + HoneyPotOEVShare oevShare; + HoneyPot honeyPot; + HoneyPotDAO honeyPotDAO; + ChronicleMedianSourceMock chronicleMock; + + function run() external { + vm.startBroadcast(); + + address chainlink = vm.envOr("CHAINLINK_SOURCE", 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); + address pyth = vm.envOr("PYTH_SOURCE", 0x4305FB66699C3B2702D4d05CF36551390A4c69C6); + + bytes32 defaultId = 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace; + bytes32 pythPriceId = vm.envOr("PYTH_PRICE_ID", bytes32(0)); + if (pythPriceId == bytes32(0)) { + pythPriceId = defaultId; + } + + // Create mock ChronicleMedianSource and set the latest source data. + chronicleMock = new ChronicleMedianSourceMock(); + + oevShare = new HoneyPotOEVShare( + chainlink, + address(chronicleMock), + pyth, + pythPriceId, + 8 + ); + + honeyPot = new HoneyPot(IAggregatorV3Source(address(oevShare))); + + honeyPotDAO = new HoneyPotDAO(); + + vm.stopBroadcast(); + } +} diff --git a/src/HoneyPotDAO.sol b/src/HoneyPotDAO.sol new file mode 100644 index 0000000..b0f4ab0 --- /dev/null +++ b/src/HoneyPotDAO.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.17; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract HoneyPotDAO is Ownable { + // Define events + event ReceivedEther(address sender, uint256 amount); + event DrainedEther(address to, uint256 amount); + + receive() external payable { + emit ReceivedEther(msg.sender, msg.value); + } + + function drain() external onlyOwner { + uint256 balance = address(this).balance; + Address.sendValue(payable(owner()), balance); + emit DrainedEther(owner(), balance); + } +} diff --git a/src/mock/ChronicleMedianSourceMock.sol b/src/mock/ChronicleMedianSourceMock.sol new file mode 100644 index 0000000..d0e148d --- /dev/null +++ b/src/mock/ChronicleMedianSourceMock.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.17; + +import {IMedian} from "oev-contracts/interfaces/chronicle/IMedian.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract ChronicleMedianSourceMock is IMedian, Ownable { + uint256 public value; + uint32 public ageValue; + + function age() external view returns (uint32) { + return ageValue; + } + + function read() external view returns (uint256) { + return value; + } + + function peek() external view returns (uint256, bool) { + return (value, true); + } + + function setLatestSourceData(uint256 _value, uint32 _age) public onlyOwner { + value = _value; + ageValue = _age; + } + + function kiss(address) external override {} +} diff --git a/test/HoneyPot.sol b/test/HoneyPot.sol index fb9fe55..7ef0df7 100644 --- a/test/HoneyPot.sol +++ b/test/HoneyPot.sol @@ -8,15 +8,23 @@ import {IPyth} from "oev-contracts/interfaces/pyth/IPyth.sol"; import {HoneyPotOEVShare} from "../src/HoneyPotOEVShare.sol"; import {HoneyPot} from "../src/HoneyPot.sol"; +import {HoneyPotDAO} from "../src/HoneyPotDAO.sol"; +import {ChronicleMedianSourceMock} from "../src/mock/ChronicleMedianSourceMock.sol"; contract HoneyPotTest is CommonTest { + event ReceivedEther(address sender, uint256 amount); + event DrainedEther(address to, uint256 amount); + IAggregatorV3Source chainlink = IAggregatorV3Source(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); IMedian chronicle = IMedian(0x64DE91F5A373Cd4c28de3600cB34C7C6cE410C85); IPyth pyth = IPyth(0x4305FB66699C3B2702D4d05CF36551390A4c69C6); bytes32 pythPriceId = 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace; + ChronicleMedianSourceMock chronicleMock; + HoneyPotOEVShare oevShare; HoneyPot honeyPot; + HoneyPotDAO honeyPotDAO; uint256 public constant liquidationPrice = 0.1e18; uint256 public honeyPotBalance = 1 ether; @@ -32,8 +40,10 @@ contract HoneyPotTest is CommonTest { ); honeyPot = new HoneyPot(IAggregatorV3Source(address(oevShare))); + honeyPotDAO = new HoneyPotDAO(); _whitelistOnChronicle(); oevShare.setUnlocker(address(this), true); + chronicleMock = new ChronicleMedianSourceMock(); } receive() external payable {} @@ -86,7 +96,7 @@ contract HoneyPotTest is CommonTest { vm.prank(liquidator); vm.expectRevert("Liquidation price reached for this user"); - honeyPot.emptyHoneyPot(address(this)); // emptyHoneyPot now requires the creator's address + honeyPot.emptyHoneyPot(address(this)); // Simulate price change mockChainlinkPriceChange(); @@ -97,7 +107,7 @@ contract HoneyPotTest is CommonTest { uint256 liquidatorBalanceBefore = liquidator.balance; vm.prank(liquidator); - honeyPot.emptyHoneyPot(address(this)); // emptyHoneyPot now requires the creator's address + honeyPot.emptyHoneyPot(address(this)); uint256 liquidatorBalanceAfter = liquidator.balance; @@ -108,4 +118,57 @@ contract HoneyPotTest is CommonTest { (, uint256 testhoneyPotBalanceTwo) = honeyPot.honeyPots(address(this)); assertTrue(testhoneyPotBalanceTwo == honeyPotBalance); } + + function testHoneyPotDAO() public { + vm.expectEmit(true, true, true, true); + emit ReceivedEther(address(this), 1 ether); + payable(address(honeyPotDAO)).transfer(1 ether); + + vm.expectEmit(true, true, true, true); + emit DrainedEther(address(this), 1 ether); + honeyPotDAO.drain(); + } + + function testChronicleMock() public { + uint32 age = chronicle.age(); + uint256 read = chronicle.read(); + chronicleMock.setLatestSourceData(read, age); + + HoneyPotOEVShare oevShare2 = new HoneyPotOEVShare( + address(chainlink), + address(chronicleMock), + address(pyth), + pythPriceId, + 8 + ); + oevShare2.setUnlocker(address(this), true); + + HoneyPot honeyPot2 = new HoneyPot( + IAggregatorV3Source(address(oevShare2)) + ); + + // Create HoneyPot for the caller + honeyPot2.createHoneyPot{value: honeyPotBalance}(); + (, uint256 testhoneyPotBalance) = honeyPot2.honeyPots(address(this)); + assertTrue(testhoneyPotBalance == honeyPotBalance); + + vm.prank(liquidator); + vm.expectRevert("Liquidation price reached for this user"); + honeyPot2.emptyHoneyPot(address(this)); + + // Simulate price change + chronicleMock.setLatestSourceData((read * 103) / 100, uint32(block.timestamp - 1)); + + // Unlock the latest value + oevShare2.unlockLatestValue(); + + uint256 liquidatorBalanceBefore = liquidator.balance; + + vm.prank(liquidator); + honeyPot2.emptyHoneyPot(address(this)); + + uint256 liquidatorBalanceAfter = liquidator.balance; + + assertTrue(liquidatorBalanceAfter == liquidatorBalanceBefore + honeyPotBalance); + } }