Skip to content

Commit

Permalink
feat: honey pot (#1)
Browse files Browse the repository at this point in the history
Signed-off-by: Pablo Maldonado <[email protected]>
  • Loading branch information
md0x authored Oct 25, 2023
1 parent 4107978 commit efa0ae5
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 58 deletions.
7 changes: 5 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -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 = [email protected]: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
46 changes: 17 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,27 @@
# <h1 align="center"> Forge Template </h1>

**Template repository for getting started quickly with Foundry projects**
# <h1 align="center"> OEV Share HoneyPot Demo </h1>

![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/<YOUR_INFURA_KEY>
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.
9 changes: 9 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -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}"
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at e50c24
4 changes: 4 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -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/
4 changes: 0 additions & 4 deletions src/Contract.sol

This file was deleted.

83 changes: 83 additions & 0 deletions src/HoneyPot.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
33 changes: 33 additions & 0 deletions src/HoneyPotOEVShare.sol
Original file line number Diff line number Diff line change
@@ -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)
{}
}
13 changes: 13 additions & 0 deletions test/Common.sol
Original file line number Diff line number Diff line change
@@ -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);
}
23 changes: 0 additions & 23 deletions test/Contract.t.sol

This file was deleted.

127 changes: 127 additions & 0 deletions test/HoneyPot.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit efa0ae5

Please sign in to comment.