diff --git a/.gitmodules b/.gitmodules index 7e7b428..80109ea 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/oval-quickstart"] path = lib/oval-quickstart url = https://github.com/UMAprotocol/oval-quickstart +[submodule "lib/oval-contracts"] + path = lib/oval-contracts + url = https://github.com/UMAprotocol/oval-contracts diff --git a/lib/oval-contracts b/lib/oval-contracts new file mode 160000 index 0000000..25afc49 --- /dev/null +++ b/lib/oval-contracts @@ -0,0 +1 @@ +Subproject commit 25afc497af220adbc580b9c4ab04ab2485f3a564 diff --git a/remappings.txt b/remappings.txt index a421151..c04852c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ oval-quickstart/=lib/oval-quickstart/src/ +oval-contracts/=lib/oval-contracts/src/ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ \ No newline at end of file diff --git a/script/HoneyPot.s.sol b/script/HoneyPot.s.sol index 920541f..261f47e 100644 --- a/script/HoneyPot.s.sol +++ b/script/HoneyPot.s.sol @@ -8,10 +8,12 @@ import {HoneyPot} from "../src/HoneyPot.sol"; import {HoneyPotDAO} from "../src/HoneyPotDAO.sol"; import {ChainlinkOvalImmutable, IAggregatorV3Source} from "oval-quickstart/ChainlinkOvalImmutable.sol"; +import {MockV3Aggregator} from "../src/mock/MockV3Aggregator.sol"; + contract HoneyPotDeploymentScript is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - address chainlink = vm.envAddress("SOURCE_ADDRESS"); + address chainlink = vm.envOr("SOURCE_ADDRESS", address(0)); uint256 lockWindow = vm.envUint("LOCK_WINDOW"); uint256 maxTraversal = vm.envUint("MAX_TRAVERSAL"); address unlocker = vm.envAddress("UNLOCKER"); @@ -24,9 +26,18 @@ contract HoneyPotDeploymentScript is Script { console.log(" - Max traversal: ", maxTraversal); console.log(" - Unlocker: ", unlockers[0]); + vm.startBroadcast(deployerPrivateKey); + + if(chainlink == address(0)) { + console.log("Chainlink source address is not set"); + console.log("Deploying a mock Chainlink source"); + MockV3Aggregator mock = new MockV3Aggregator(8, 100000000000); + chainlink = address(mock); + console.log("Deployed mock Chainlink source at address: ", chainlink); + } + IAggregatorV3Source source = IAggregatorV3Source(chainlink); uint8 decimals = IAggregatorV3Source(chainlink).decimals(); - vm.startBroadcast(deployerPrivateKey); ChainlinkOvalImmutable oracle = new ChainlinkOvalImmutable(source, decimals, lockWindow, maxTraversal, unlockers); diff --git a/src/HoneyPot.sol b/src/HoneyPot.sol index 809d77c..ddd895d 100644 --- a/src/HoneyPot.sol +++ b/src/HoneyPot.sol @@ -15,9 +15,9 @@ contract HoneyPot is Ownable { IAggregatorV3Source public oracle; // Oval 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); + event HoneyPotCreated(address indexed owner, int256 initialPrice, uint256 amount); + event HoneyPotEmptied(address indexed owner, address indexed liquidator, uint256 amount); + event HoneyPotReset(address indexed owner, uint256 amount); constructor(IAggregatorV3Source _oracle) { oracle = _oracle; @@ -40,8 +40,8 @@ contract HoneyPot is Ownable { emit HoneyPotCreated(msg.sender, currentPrice, msg.value); } - function _emptyPotForUser(address honeyPotCreator, address recipient) internal returns (uint256 amount) { - HoneyPotDetails storage userPot = honeyPots[honeyPotCreator]; + function _emptyPotForUser(address owner, address recipient) internal returns (uint256 amount) { + HoneyPotDetails storage userPot = honeyPots[owner]; amount = userPot.balance; userPot.balance = 0; // reset the balance @@ -49,21 +49,21 @@ contract HoneyPot is Ownable { Address.sendValue(payable(recipient), amount); } - function emptyHoneyPot(address honeyPotCreator) external { + function emptyHoneyPot(address owner) external { (, int256 currentPrice,,,) = oracle.latestRoundData(); require(currentPrice >= 0, "Invalid price from oracle"); - HoneyPotDetails storage userPot = honeyPots[honeyPotCreator]; + HoneyPotDetails storage userPot = honeyPots[owner]; require(currentPrice != userPot.liquidationPrice, "Liquidation price reached for this user"); require(userPot.balance > 0, "No balance to withdraw"); - uint256 withdrawnAmount = _emptyPotForUser(honeyPotCreator, msg.sender); - emit HoneyPotEmptied(honeyPotCreator, msg.sender, withdrawnAmount); + uint256 withdrawnAmount = _emptyPotForUser(owner, msg.sender); + emit HoneyPotEmptied(owner, msg.sender, withdrawnAmount); } function resetPot() external { uint256 withdrawnAmount = _emptyPotForUser(msg.sender, msg.sender); - emit PotReset(msg.sender, withdrawnAmount); + emit HoneyPotReset(msg.sender, withdrawnAmount); } } diff --git a/src/mock/MockV3Aggregator.sol b/src/mock/MockV3Aggregator.sol new file mode 100644 index 0000000..745977c --- /dev/null +++ b/src/mock/MockV3Aggregator.sol @@ -0,0 +1,83 @@ +pragma solidity ^0.8.0; + +import {IAggregatorV3} from "oval-contracts/interfaces/chainlink/IAggregatorV3.sol"; + +contract MockV3Aggregator is IAggregatorV3 { + + event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); + + address public immutable aggregator; + uint256 public constant version = 0; + + uint8 public override decimals; + int256 public override latestAnswer; + uint256 public override latestTimestamp; + uint256 public latestRound; + + mapping(uint256 => int256) public getAnswer; + mapping(uint256 => uint256) public getTimestamp; + mapping(uint256 => uint256) private getStartedAt; + + constructor(uint8 _decimals, int256 _initialAnswer) { + decimals = _decimals; + updateAnswer(_initialAnswer); + aggregator = address(this); // For simplicity, we set the aggregator address to the contract address + } + + function updateAnswer(int256 _answer) internal { + latestAnswer = _answer; + latestTimestamp = block.timestamp; + latestRound++; + getAnswer[latestRound] = _answer; + getTimestamp[latestRound] = block.timestamp; + getStartedAt[latestRound] = block.timestamp; + + emit AnswerUpdated(_answer, latestRound, block.timestamp); + } + + // This function is part of the AccessControlledOffchainAggregator interface. It is used to + // update the new price and look like a transmit function from a real Chainlink Aggregator + function transmit( + bytes calldata _report, // _report should be the ABI encoded int256 new answer + bytes32[] calldata _rs, bytes32[] calldata _ss, bytes32 _rawVs + ) + external + { + int256 newAnswer = abi.decode(_report, (int256)); + updateAnswer(newAnswer); + } + + function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public { + latestRound = _roundId; + latestAnswer = _answer; + latestTimestamp = _timestamp; + getAnswer[latestRound] = _answer; + getTimestamp[latestRound] = _timestamp; + getStartedAt[latestRound] = _startedAt; + } + + function getRoundData( + uint80 _roundId + ) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId); + } + + function latestRoundData() + external + view + override + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + return ( + uint80(latestRound), + getAnswer[latestRound], + getStartedAt[latestRound], + getTimestamp[latestRound], + uint80(latestRound) + ); + } +} diff --git a/test/HoneyPot.sol b/test/HoneyPot.sol index a1cb51e..fb09ef4 100644 --- a/test/HoneyPot.sol +++ b/test/HoneyPot.sol @@ -6,13 +6,15 @@ import {HoneyPot} from "../src/HoneyPot.sol"; import {HoneyPotDAO} from "../src/HoneyPotDAO.sol"; import {ChainlinkOvalImmutable, IAggregatorV3Source} from "oval-quickstart/ChainlinkOvalImmutable.sol"; +import {MockV3Aggregator} from "../src/mock/MockV3Aggregator.sol"; + contract HoneyPotTest is CommonTest { event ReceivedEther(address sender, uint256 amount); event DrainedEther(address to, uint256 amount); 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); + event HoneyPotCreated(address indexed owner, int256 initialPrice, uint256 initialBalance); + event HoneyPotEmptied(address indexed owner, address indexed liquidator, uint256 amount); + event HoneyPotReset(address indexed owner, uint256 amount); IAggregatorV3Source chainlink = IAggregatorV3Source(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); ChainlinkOvalImmutable oracle; @@ -63,7 +65,7 @@ contract HoneyPotTest is CommonTest { // Reset HoneyPot for the caller vm.expectEmit(true, true, true, true); - emit PotReset(address(this), honeyPotBalance); + emit HoneyPotReset(address(this), honeyPotBalance); honeyPot.resetPot(); (, uint256 testhoneyPotBalanceReset) = honeyPot.honeyPots(address(this)); assertTrue(testhoneyPotBalanceReset == 0); @@ -106,6 +108,42 @@ contract HoneyPotTest is CommonTest { assertTrue(testhoneyPotBalanceTwo == honeyPotBalance); } + function testCrackHoneyPotWithMockOracle() public { + // Setup mock oracle + MockV3Aggregator mock = new MockV3Aggregator(8, 100000000000); + address[] memory unlockers = new address[](1); + unlockers[0] = address(this); + oracle = new ChainlinkOvalImmutable(IAggregatorV3Source(address(mock)), 8, 3, 10, unlockers); + honeyPot = new HoneyPot(IAggregatorV3Source(address(oracle))); + + // Create HoneyPot for the caller + (, int256 currentPrice,,,) = oracle.latestRoundData(); + vm.expectEmit(true, true, true, true); + emit HoneyPotCreated(address(this), currentPrice, honeyPotBalance); + honeyPot.createHoneyPot{value: honeyPotBalance}(); + (, uint256 testhoneyPotBalance) = honeyPot.honeyPots(address(this)); + assertTrue(testhoneyPotBalance == honeyPotBalance); + + // Simulate price change + int256 newAnswer = (currentPrice * 103) / 100; + bytes32[] memory empty = new bytes32[](0); + mock.transmit(abi.encode(newAnswer), empty, empty, bytes32(0)); + + // Unlock the latest value + oracle.unlockLatestValue(); + + uint256 liquidatorBalanceBefore = liquidator.balance; + + vm.prank(liquidator); + vm.expectEmit(true, true, true, true); + emit HoneyPotEmptied(address(this), liquidator, honeyPotBalance); + honeyPot.emptyHoneyPot(address(this)); + + uint256 liquidatorBalanceAfter = liquidator.balance; + + assertTrue(liquidatorBalanceAfter == liquidatorBalanceBefore + honeyPotBalance); + } + function testHoneyPotDAO() public { vm.expectEmit(true, true, true, true); emit ReceivedEther(address(this), 1 ether);