diff --git a/.gitmodules b/.gitmodules
index d72d544..b83ed6d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,9 @@
[submodule "lib/forge-std"]
path = lib/forge-std
- url = git@github.com:foundry-rs/forge-std.git
+ url = https://github.com/foundry-rs/forge-std.git
[submodule "lib/oev-contracts"]
path = lib/oev-contracts
- url = git@github.com:UMAprotocol/oev-contracts.git
+ url = https://github.com/UMAprotocol/oev-contracts.git
+[submodule "lib/openzeppelin-contracts"]
+ path = lib/openzeppelin-contracts
+ url = https://github.com/OpenZeppelin/openzeppelin-contracts
diff --git a/README.md b/README.md
index efac030..9457853 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,27 @@
-#
Forge Template
-**Template repository for getting started quickly with Foundry projects**
+# OEV Share HoneyPot Demo
-![Github Actions](https://github.com/foundry-rs/forge-template/workflows/CI/badge.svg)
+**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.**
-## Getting Started
-
-Click "Use this template" on [GitHub](https://github.com/foundry-rs/forge-template) to create a new repository with this repo as the initial state.
-
-Or, if your repo already exists, run:
-```sh
-forge init
-forge build
-forge test
-```
+![Github Actions](https://github.com/UMAprotocol/oev-demo/workflows/CI/badge.svg)
-## Writing your first test
+## Introduction
-All you need is to `import forge-std/Test.sol` and then inherit it from your test contract. Forge-std's Test contract comes with a pre-instatiated [cheatcodes environment](https://book.getfoundry.sh/cheatcodes/), the `vm`. It also has support for [ds-test](https://book.getfoundry.sh/reference/ds-test.html)-style logs and assertions. Finally, it supports Hardhat's [console.log](https://github.com/brockelmore/forge-std/blob/master/src/console.sol). The logging functionalities require `-vvvv`.
+The HoneyPot mechanism is a unique setup where funds are kept in a contract that is designed to be emptied out based on specific criteria, in this case, a change in the price from an oracle.
-```solidity
-pragma solidity 0.8.10;
-
-import "forge-std/Test.sol";
+## Getting Started
-contract ContractTest is Test {
- function testExample() public {
- vm.roll(100);
- console.log(1);
- emit log("hi");
- assertTrue(true);
- }
-}
+To test the demo run the following commands:
+```
+forge install
+export RPC_MAINNET=https://mainnet.infura.io/v3/
+forge test`
```
-## Development
+## Contracts Overview
-This project uses [Foundry](https://getfoundry.sh). See the [book](https://book.getfoundry.sh/getting-started/installation.html) for instructions on how to install and use Foundry.
+- **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
diff --git a/foundry.toml b/foundry.toml
index 93d2364..16a3581 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -1,2 +1,11 @@
+[profile.default]
+src = "src"
+out = "out"
+libs = ["lib"]
+solc = "0.8.17"
+
[profile.ci.fuzz]
runs = 10_000
+
+[rpc_endpoints]
+mainnet = "${RPC_MAINNET}"
\ No newline at end of file
diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts
new file mode 160000
index 0000000..e50c24f
--- /dev/null
+++ b/lib/openzeppelin-contracts
@@ -0,0 +1 @@
+Subproject commit e50c24f5839db17f46991478384bfda14acfb830
diff --git a/remappings.txt b/remappings.txt
new file mode 100644
index 0000000..2053123
--- /dev/null
+++ b/remappings.txt
@@ -0,0 +1,4 @@
+ds-test/=lib/forge-std/lib/ds-test/src/
+forge-std/=lib/forge-std/src/
+oev-contracts/=lib/oev-contracts/src/
+@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
\ No newline at end of file
diff --git a/src/Contract.sol b/src/Contract.sol
deleted file mode 100644
index 45cf848..0000000
--- a/src/Contract.sol
+++ /dev/null
@@ -1,4 +0,0 @@
-// SPDX-License-Identifier: Unlicense
-pragma solidity ^0.8.13;
-
-contract Contract { }
diff --git a/src/HoneyPot.sol b/src/HoneyPot.sol
new file mode 100644
index 0000000..8a3c88e
--- /dev/null
+++ b/src/HoneyPot.sol
@@ -0,0 +1,83 @@
+// 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";
+import {IAggregatorV3Source} from "oev-contracts/interfaces/chainlink/IAggregatorV3Source.sol";
+
+contract HoneyPot is Ownable {
+ struct HoneyPotDetails {
+ int256 liquidationPrice;
+ uint256 balance;
+ }
+
+ mapping(address => HoneyPotDetails) public honeyPots;
+ IAggregatorV3Source public oracle; // OEV Share serving as a Chainlink oracle
+
+ event OracleUpdated(address indexed newOracle);
+ event HoneyPotCreated(
+ address indexed creator,
+ int256 liquidationPrice,
+ uint256 initialBalance
+ );
+ event HoneyPotEmptied(
+ address indexed honeyPotCreator,
+ address indexed trigger,
+ uint256 amount
+ );
+ event PotReset(address indexed owner, uint256 amount);
+
+ constructor(IAggregatorV3Source _oracle) {
+ oracle = _oracle;
+ }
+
+ function setOracle(IAggregatorV3Source _oracle) external onlyOwner {
+ oracle = _oracle;
+ emit OracleUpdated(address(_oracle));
+ }
+
+ function createHoneyPot(int256 _liquidationPrice) external payable {
+ require(
+ honeyPots[msg.sender].liquidationPrice == 0,
+ "Liquidation price already set for this user"
+ );
+ require(_liquidationPrice > 0, "Liquidation price cannot be zero");
+
+ honeyPots[msg.sender].liquidationPrice = _liquidationPrice;
+ honeyPots[msg.sender].balance = msg.value;
+
+ emit HoneyPotCreated(msg.sender, _liquidationPrice, msg.value);
+ }
+
+ function _emptyPotForUser(
+ address honeyPotCreator,
+ address recipient
+ ) internal {
+ HoneyPotDetails storage userPot = honeyPots[honeyPotCreator];
+
+ uint256 amount = userPot.balance;
+ userPot.balance = 0; // reset the balance
+ userPot.liquidationPrice = 0; // reset the liquidation price
+ Address.sendValue(payable(recipient), amount);
+ }
+
+ function emptyHoneyPot(address honeyPotCreator) external {
+ (, int256 currentPrice, , , ) = oracle.latestRoundData();
+ require(currentPrice >= 0, "Invalid price from oracle");
+
+ HoneyPotDetails storage userPot = honeyPots[honeyPotCreator];
+
+ require(
+ currentPrice != userPot.liquidationPrice,
+ "Liquidation price reached for this user"
+ );
+
+ _emptyPotForUser(honeyPotCreator, msg.sender);
+ emit HoneyPotEmptied(honeyPotCreator, msg.sender, userPot.balance);
+ }
+
+ function resetPot() external {
+ _emptyPotForUser(msg.sender, msg.sender);
+ emit PotReset(msg.sender, honeyPots[msg.sender].balance);
+ }
+}
diff --git a/src/HoneyPotOEVShare.sol b/src/HoneyPotOEVShare.sol
new file mode 100644
index 0000000..997a4cb
--- /dev/null
+++ b/src/HoneyPotOEVShare.sol
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity 0.8.17;
+
+import {BoundedUnionSourceAdapter} from "oev-contracts/adapters/source-adapters/BoundedUnionSourceAdapter.sol";
+import {BaseController} from "oev-contracts/controllers/BaseController.sol";
+import {ChainlinkDestinationAdapter} from "oev-contracts/adapters/destination-adapters/ChainlinkDestinationAdapter.sol";
+import {IAggregatorV3Source} from "oev-contracts/interfaces/chainlink/IAggregatorV3Source.sol";
+import {IMedian} from "oev-contracts/interfaces/chronicle/IMedian.sol";
+import {IPyth} from "oev-contracts/interfaces/pyth/IPyth.sol";
+
+contract HoneyPotOEVShare is
+ BaseController,
+ BoundedUnionSourceAdapter,
+ ChainlinkDestinationAdapter
+{
+ constructor(
+ address chainlinkSource,
+ address chronicleSource,
+ address pythSource,
+ bytes32 pythPriceId,
+ uint8 decimals
+ )
+ BoundedUnionSourceAdapter(
+ IAggregatorV3Source(chainlinkSource),
+ IMedian(chronicleSource),
+ IPyth(pythSource),
+ pythPriceId,
+ 0.1e18
+ )
+ BaseController()
+ ChainlinkDestinationAdapter(decimals)
+ {}
+}
diff --git a/test/Common.sol b/test/Common.sol
new file mode 100644
index 0000000..cc7995d
--- /dev/null
+++ b/test/Common.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity 0.8.17;
+
+import {Test} from "forge-std/Test.sol";
+
+contract CommonTest is Test {
+ address public constant owner = address(0x1);
+ address public constant permissionedUnlocker = address(0x2);
+ address public constant liquidator = address(0x3);
+ address public constant account1 = address(0x4);
+ address public constant account2 = address(0x5);
+ address public constant random = address(0x6);
+}
diff --git a/test/Contract.t.sol b/test/Contract.t.sol
deleted file mode 100644
index d7d6076..0000000
--- a/test/Contract.t.sol
+++ /dev/null
@@ -1,23 +0,0 @@
-// SPDX-License-Identifier: Unlicense
-pragma solidity ^0.8.13;
-
-import "forge-std/Test.sol";
-
-import "src/Contract.sol";
-
-contract TestContract is Test {
- Contract c;
-
- function setUp() public {
- c = new Contract();
- }
-
- function testBar() public {
- assertEq(uint256(1), uint256(1), "ok");
- }
-
- function testFoo(uint256 x) public {
- vm.assume(x < type(uint128).max);
- assertEq(x + x, x * 2);
- }
-}
diff --git a/test/HoneyPot.sol b/test/HoneyPot.sol
new file mode 100644
index 0000000..5d01bec
--- /dev/null
+++ b/test/HoneyPot.sol
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity 0.8.17;
+
+import {CommonTest} from "./Common.sol";
+import {IAggregatorV3Source} from "oev-contracts/interfaces/chainlink/IAggregatorV3Source.sol";
+import {IMedian} from "oev-contracts/interfaces/chronicle/IMedian.sol";
+import {IPyth} from "oev-contracts/interfaces/pyth/IPyth.sol";
+
+import {HoneyPotOEVShare} from "../src/HoneyPotOEVShare.sol";
+import {HoneyPot} from "../src/HoneyPot.sol";
+
+contract HoneyPotTest is CommonTest {
+ IAggregatorV3Source chainlink =
+ IAggregatorV3Source(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
+ IMedian chronicle = IMedian(0x64DE91F5A373Cd4c28de3600cB34C7C6cE410C85);
+ IPyth pyth = IPyth(0x4305FB66699C3B2702D4d05CF36551390A4c69C6);
+ bytes32 pythPriceId =
+ 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace;
+
+ HoneyPotOEVShare oevShare;
+ HoneyPot honeyPot;
+
+ uint256 public constant liquidationPrice = 0.1e18;
+ uint256 public honeyPotBalance = 1 ether;
+
+ function setUp() public {
+ vm.createSelectFork("mainnet", 18419040); // Recent block on mainnet
+ oevShare = new HoneyPotOEVShare(
+ address(chainlink),
+ address(chronicle),
+ address(pyth),
+ pythPriceId,
+ 8
+ );
+
+ honeyPot = new HoneyPot(IAggregatorV3Source(address(oevShare)));
+ _whitelistOnChronicle();
+ oevShare.setUnlocker(address(this), true);
+ }
+
+ receive() external payable {}
+
+ function _whitelistOnChronicle() internal {
+ vm.startPrank(0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB); // DSPause that is a ward (can add kiss to chronicle)
+ chronicle.kiss(address(oevShare));
+ chronicle.kiss(address(this)); // So that we can read Chronicle directly.
+ vm.stopPrank();
+ }
+
+ function mockChainlinkPriceChange() public {
+ (
+ uint80 roundId,
+ int256 answer,
+ uint256 startedAt,
+ uint256 updatedAt,
+ uint80 answeredInRound
+ ) = chainlink.latestRoundData();
+ vm.mockCall(
+ address(chainlink),
+ abi.encodeWithSelector(chainlink.latestRoundData.selector),
+ abi.encode(
+ roundId + 1,
+ (answer * 103) / 100, // 3% increase
+ startedAt + 1,
+ updatedAt + 1,
+ answeredInRound + 1
+ )
+ );
+ }
+
+ function testHoneyPotCreationAndReset() public {
+ uint256 balanceBefore = address(this).balance;
+
+ // Create HoneyPot for the caller
+ (, int256 latestAnswer, , , ) = oevShare.latestRoundData();
+ honeyPot.createHoneyPot{value: honeyPotBalance}(latestAnswer);
+
+ (, uint256 testhoneyPotBalance) = honeyPot.honeyPots(address(this));
+ assertTrue(testhoneyPotBalance == honeyPotBalance);
+ assertTrue(address(this).balance == balanceBefore - honeyPotBalance);
+
+ // Reset HoneyPot for the caller
+ honeyPot.resetPot();
+ (, uint256 testhoneyPotBalanceReset) = honeyPot.honeyPots(
+ address(this)
+ );
+ assertTrue(testhoneyPotBalanceReset == 0);
+ assertTrue(address(this).balance == balanceBefore);
+ }
+
+ function testCrackHoneyPot() public {
+ // Create HoneyPot for the caller
+ (, int256 latestAnswer, , , ) = oevShare.latestRoundData();
+ honeyPot.createHoneyPot{value: honeyPotBalance}(
+ latestAnswer
+ );
+ (, uint256 testhoneyPotBalance) = honeyPot.honeyPots(address(this));
+ assertTrue(testhoneyPotBalance == honeyPotBalance);
+
+ vm.prank(liquidator);
+ vm.expectRevert("Liquidation price reached for this user");
+ honeyPot.emptyHoneyPot(address(this)); // emptyHoneyPot now requires the creator's address
+
+ // Simulate price change
+ mockChainlinkPriceChange();
+
+ // Unlock the latest value
+ oevShare.unlockLatestValue();
+
+ uint256 liquidatorBalanceBefore = liquidator.balance;
+
+ vm.prank(liquidator);
+ honeyPot.emptyHoneyPot(address(this)); // emptyHoneyPot now requires the creator's address
+
+ uint256 liquidatorBalanceAfter = liquidator.balance;
+
+ assertTrue(
+ liquidatorBalanceAfter == liquidatorBalanceBefore + honeyPotBalance
+ );
+
+ // Create HoneyPot can be called again
+ (, int256 latestAnswerNew, , , ) = oevShare.latestRoundData();
+ honeyPot.createHoneyPot{value: honeyPotBalance}(latestAnswerNew);
+ (, uint256 testhoneyPotBalanceTwo) = honeyPot.honeyPots(address(this));
+ assertTrue(testhoneyPotBalanceTwo == honeyPotBalance);
+ }
+}