Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: honey pot #1

Merged
merged 7 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = git@github.com:UMAprotocol/oev-contracts.git
url = https://github.com/UMAprotocol/oev-contracts.git
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Failing for now in CI as this repo doesn't have access

Copy link
Member

Choose a reason for hiding this comment

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

@md0x we should be able to give github ci access to private repos. Do you know how to do this?

I can add keys to the ci environment if needed!

[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`
Copy link
Member

Choose a reason for hiding this comment

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

nit: for consistency with other repos, can we use NODE_URL_1?

Copy link
Member

Choose a reason for hiding this comment

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

This is not consistent with oev-contracts repo ;)

Copy link
Member

Choose a reason for hiding this comment

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

Well, it should be consistent across all of our repos if possible!! Seems easier to change these two than the rest :).

I chatted with @md0x offline, and I think we'll decide on this and make a change in a separate PR.

```

## 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.

38 changes: 38 additions & 0 deletions src/HoneyPot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.17;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {HoneyPotOEVShare} from "./HoneyPotOEVShare.sol";

contract HoneyPot is Ownable {
uint256 public liquidationPrice;
HoneyPotOEVShare public oracle;
Copy link
Member

Choose a reason for hiding this comment

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

nit: to better demonstrate that this contract is talking to something that looks like chainlink (or some other oracle type), what do you think about changing this to AggregatorV3Source (and we can add a comment that notes that this is the OEVShareContract).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree!


constructor(HoneyPotOEVShare _oracle) {
oracle = _oracle;
}

function setOracle(HoneyPotOEVShare _oracle) external onlyOwner {
oracle = _oracle;
}

function createHoneyPot(uint _liquidationPrice) external payable {
Copy link
Member

Choose a reason for hiding this comment

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

this design means there can only be one honey pot at a time. this might not be ideal. we might want to enable multiple to exist simultaneously enabling multiple people to demo the app at the same time. this would require us to have a mapping, rather than this relationship.

thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think that's easy to implement and will be very helpful during the demo. I've updated the contract and tests accordingly. Thanks you!

Copy link
Member

Choose a reason for hiding this comment

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

nit: why not make this an int256 (since this is what the oracle returns)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree!

require(liquidationPrice == 0, "Liquidation price already set");
liquidationPrice = _liquidationPrice;
}

function emptyHoneyPot() external {
int256 currentPrice = oracle.latestAnswer();
require(currentPrice >= 0, "Invalid price from oracle");
require(
uint256(currentPrice) != liquidationPrice,
"Liquidation price reached"
);
payable(msg.sender).transfer(address(this).balance);
}
Copy link
Member

Choose a reason for hiding this comment

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

should we not set the liquidation price back to 0 when the emptyHoneyPot is called enabling someone to create a new honey pot right after the previous one is liquidated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree @chrismaree. Fixed!


function resetPot() external onlyOwner {
Copy link
Member

Choose a reason for hiding this comment

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

Does this mean the owner can only empty their own honeypot? What about the other addresses that deposit funds?

I think there are probably two setups that make sense:

  • Any creator can withdraw their own honeypot.
  • The owner can withdraw anyone's honeypot.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right! I forgot to remove the onlyOwner there so creators can withdraw their pots.
Updated.

liquidationPrice = 0;
payable(msg.sender).transfer(address(this).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.

111 changes: 111 additions & 0 deletions test/HoneyPot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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(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;
honeyPot.createHoneyPot{value: honeyPotBalance}(
uint256(oevShare.latestAnswer())
);
assertTrue(address(honeyPot).balance == honeyPotBalance);
assertTrue(address(this).balance == balanceBefore - honeyPotBalance);

honeyPot.resetPot();
assertTrue(address(honeyPot).balance == 0);
assertTrue(address(this).balance == balanceBefore);
}

function testCrackHoneyPot() public {
honeyPot.createHoneyPot{value: honeyPotBalance}(
uint256(oevShare.latestAnswer())
);

assertTrue(address(honeyPot).balance == honeyPotBalance);

vm.prank(liquidator);
vm.expectRevert("Liquidation price reached");
honeyPot.emptyHoneyPot();

// Simulate price change
mockChainlinkPriceChange();

// Unlock the latest value
oevShare.unlockLatestValue();

uint256 liquidatorBalanceBefore = liquidator.balance;
vm.prank(liquidator);
honeyPot.emptyHoneyPot();

uint256 liquidatorBalanceAfter = liquidator.balance;

assertTrue(
liquidatorBalanceAfter == liquidatorBalanceBefore + honeyPotBalance
);
}
}
Loading