-
Notifications
You must be signed in to change notification settings - Fork 1
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
Changes from 5 commits
8aa89d6
d058c92
6ff0c90
4d8b85f
b24a376
954fe9c
2a9674f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
[submodule "lib/openzeppelin-contracts"] | ||
path = lib/openzeppelin-contracts | ||
url = https://github.com/OpenZeppelin/openzeppelin-contracts |
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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: for consistency with other repos, can we use NODE_URL_1? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not consistent with oev-contracts repo ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
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}" |
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/ |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// 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 { | ||
struct HoneyPotDetails { | ||
uint256 liquidationPrice; | ||
uint256 balance; | ||
} | ||
|
||
mapping(address => HoneyPotDetails) public honeyPots; | ||
HoneyPotOEVShare public oracle; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree! |
||
|
||
// Declare events | ||
event OracleUpdated(address indexed newOracle); | ||
event HoneyPotCreated( | ||
address indexed creator, | ||
uint256 liquidationPrice, | ||
uint256 initialBalance | ||
); | ||
event HoneyPotEmptied( | ||
address indexed honeyPotCreator, | ||
address indexed trigger, | ||
uint256 amount | ||
); | ||
event PotReset(address indexed owner, uint256 amount); | ||
|
||
constructor(HoneyPotOEVShare _oracle) { | ||
oracle = _oracle; | ||
} | ||
|
||
function setOracle(HoneyPotOEVShare _oracle) external onlyOwner { | ||
oracle = _oracle; | ||
emit OracleUpdated(address(_oracle)); // Emit event | ||
} | ||
|
||
function createHoneyPot(uint _liquidationPrice) external payable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree! |
||
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; // add the sent ether to the user's honey pot balance | ||
|
||
emit HoneyPotCreated(msg.sender, _liquidationPrice, msg.value); // Emit event | ||
} | ||
|
||
function emptyHoneyPot(address honeyPotCreator) external { | ||
int256 currentPrice = oracle.latestAnswer(); | ||
require(currentPrice >= 0, "Invalid price from oracle"); | ||
|
||
HoneyPotDetails storage userPot = honeyPots[honeyPotCreator]; | ||
|
||
require( | ||
uint256(currentPrice) != userPot.liquidationPrice, | ||
"Liquidation price reached for this user" | ||
); | ||
|
||
uint256 amount = userPot.balance; | ||
userPot.balance = 0; // reset the balance | ||
userPot.liquidationPrice = 0; // reset the liquidation price. There was a mistake in the original, missing the assignment. | ||
payable(msg.sender).transfer(amount); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: prefer OZ's sendValue function (safer if gas costs change in the future or a contract is the recipient): https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7c8b7a27284f503ce8ae23d63ac9403096dcf6fe/contracts/utils/Address.sol#L41 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. Updated |
||
|
||
emit HoneyPotEmptied(honeyPotCreator, msg.sender, amount); // Emit event | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we not set the liquidation price back to 0 when the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree @chrismaree. Fixed! |
||
|
||
function resetPot() external onlyOwner { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
HoneyPotDetails storage userPot = honeyPots[msg.sender]; | ||
|
||
userPot.liquidationPrice = 0; | ||
uint256 amount = userPot.balance; | ||
userPot.balance = 0; // reset the balance | ||
payable(msg.sender).transfer(amount); | ||
|
||
emit PotReset(msg.sender, amount); // Emit event | ||
} | ||
} |
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) | ||
{} | ||
} |
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); | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// 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; | ||
|
||
// Create HoneyPot for the caller | ||
honeyPot.createHoneyPot{value: honeyPotBalance}( | ||
uint256(oevShare.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 | ||
honeyPot.createHoneyPot{value: honeyPotBalance}( | ||
uint256(oevShare.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 | ||
honeyPot.createHoneyPot{value: honeyPotBalance}( | ||
uint256(oevShare.latestAnswer()) | ||
); | ||
(, uint256 testhoneyPotBalanceTwo) = honeyPot.honeyPots(address(this)); | ||
assertTrue(testhoneyPotBalanceTwo == honeyPotBalance); | ||
} | ||
} |
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.
Failing for now in CI as this repo doesn't have access
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.
@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!