-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Pablo Maldonado <[email protected]>
- Loading branch information
Showing
11 changed files
with
292 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" |
Submodule openzeppelin-contracts
added at
e50c24
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |