Add Oracle Aggregator and CCIP Integration
- Introduced Aggregator.sol for Chainlink-compatible oracle functionality, including round-based updates and access control. - Added OracleWithCCIP.sol to extend Aggregator with CCIP cross-chain messaging capabilities. - Created .gitmodules to include OpenZeppelin contracts as a submodule. - Developed a comprehensive deployment guide in NEXT_STEPS_COMPLETE_GUIDE.md for Phase 2 and smart contract deployment. - Implemented Vite configuration for the orchestration portal, supporting both Vue and React frameworks. - Added server-side logic for the Multi-Cloud Orchestration Portal, including API endpoints for environment management and monitoring. - Created scripts for resource import and usage validation across non-US regions. - Added tests for CCIP error handling and integration to ensure robust functionality. - Included various new files and directories for the orchestration portal and deployment scripts.
This commit is contained in:
107
test/e2e/ContractDeployment.t.sol
Normal file
107
test/e2e/ContractDeployment.t.sol
Normal file
@@ -0,0 +1,107 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {Aggregator} from "../../contracts/oracle/Aggregator.sol";
|
||||
import {CCIPRouter} from "../../contracts/ccip/CCIPRouter.sol";
|
||||
import {CCIPSender} from "../../contracts/ccip/CCIPSender.sol";
|
||||
import {CCIPReceiver} from "../../contracts/ccip/CCIPReceiver.sol";
|
||||
|
||||
contract ContractDeploymentTest is Test {
|
||||
address public admin;
|
||||
address public transmitter;
|
||||
address public linkToken;
|
||||
|
||||
function setUp() public {
|
||||
admin = address(this);
|
||||
transmitter = address(0x123);
|
||||
linkToken = address(0x456);
|
||||
}
|
||||
|
||||
function testDeployAggregator() public {
|
||||
Aggregator aggregator = new Aggregator(
|
||||
"ETH/USD",
|
||||
admin,
|
||||
60,
|
||||
50
|
||||
);
|
||||
|
||||
assertEq(aggregator.description(), "ETH/USD");
|
||||
assertEq(aggregator.admin(), admin);
|
||||
assertEq(aggregator.heartbeat(), 60);
|
||||
assertEq(aggregator.deviationThreshold(), 50);
|
||||
}
|
||||
|
||||
function testDeployCCIPRouter() public {
|
||||
uint256 baseFee = 1 ether;
|
||||
uint256 dataFeePerByte = 1000;
|
||||
CCIPRouter router = new CCIPRouter(linkToken, baseFee, dataFeePerByte);
|
||||
|
||||
assertEq(router.admin(), admin);
|
||||
assertEq(router.baseFee(), baseFee);
|
||||
assertEq(router.dataFeePerByte(), dataFeePerByte);
|
||||
assertEq(router.feeToken(), linkToken);
|
||||
}
|
||||
|
||||
function testDeployCCIPSender() public {
|
||||
uint256 baseFee = 1 ether;
|
||||
uint256 dataFeePerByte = 1000;
|
||||
address router = address(new CCIPRouter(linkToken, baseFee, dataFeePerByte));
|
||||
address aggregator = address(new Aggregator("ETH/USD", admin, 60, 50));
|
||||
|
||||
CCIPSender sender = new CCIPSender(router, aggregator, linkToken);
|
||||
|
||||
assertEq(address(sender.ccipRouter()), router);
|
||||
assertEq(sender.oracleAggregator(), aggregator);
|
||||
assertEq(sender.feeToken(), linkToken);
|
||||
}
|
||||
|
||||
function testDeployCCIPReceiver() public {
|
||||
uint256 baseFee = 1 ether;
|
||||
uint256 dataFeePerByte = 1000;
|
||||
address router = address(new CCIPRouter(linkToken, baseFee, dataFeePerByte));
|
||||
address aggregator = address(new Aggregator("ETH/USD", admin, 60, 50));
|
||||
|
||||
CCIPReceiver receiver = new CCIPReceiver(router, aggregator);
|
||||
|
||||
assertEq(address(receiver.router()), router);
|
||||
assertEq(receiver.oracleAggregator(), aggregator);
|
||||
}
|
||||
|
||||
function testFullDeploymentFlow() public {
|
||||
// Deploy aggregator
|
||||
Aggregator aggregator = new Aggregator("ETH/USD", admin, 60, 50);
|
||||
|
||||
// Deploy CCIP router
|
||||
uint256 baseFee = 1 ether;
|
||||
uint256 dataFeePerByte = 1000;
|
||||
CCIPRouter router = new CCIPRouter(linkToken, baseFee, dataFeePerByte);
|
||||
|
||||
// Deploy CCIP sender
|
||||
CCIPSender sender = new CCIPSender(address(router), address(aggregator), linkToken);
|
||||
|
||||
// Deploy CCIP receiver
|
||||
CCIPReceiver receiver = new CCIPReceiver(address(router), address(aggregator));
|
||||
|
||||
// Verify all contracts deployed
|
||||
assertTrue(address(aggregator) != address(0));
|
||||
assertTrue(address(router) != address(0));
|
||||
assertTrue(address(sender) != address(0));
|
||||
assertTrue(address(receiver) != address(0));
|
||||
}
|
||||
|
||||
function testDeploymentWithConfiguration() public {
|
||||
// Deploy with custom configuration
|
||||
Aggregator aggregator = new Aggregator("BTC/USD", admin, 120, 100);
|
||||
|
||||
// Add transmitter
|
||||
aggregator.addTransmitter(transmitter);
|
||||
|
||||
// Verify configuration
|
||||
assertEq(aggregator.description(), "BTC/USD");
|
||||
assertEq(aggregator.heartbeat(), 120);
|
||||
assertEq(aggregator.deviationThreshold(), 100);
|
||||
assertTrue(aggregator.isTransmitter(transmitter));
|
||||
}
|
||||
}
|
||||
|
||||
118
test/e2e/CrossChainOracle.t.sol
Normal file
118
test/e2e/CrossChainOracle.t.sol
Normal file
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {CCIPSender} from "../../contracts/ccip/CCIPSender.sol";
|
||||
import {CCIPReceiver} from "../../contracts/ccip/CCIPReceiver.sol";
|
||||
import {Aggregator} from "../../contracts/oracle/Aggregator.sol";
|
||||
import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol";
|
||||
|
||||
contract CrossChainOracleTest is Test {
|
||||
CCIPSender public sender;
|
||||
CCIPReceiver public receiver;
|
||||
Aggregator public sourceAggregator;
|
||||
Aggregator public destAggregator;
|
||||
address public mockRouter;
|
||||
address public linkToken;
|
||||
|
||||
uint64 constant SOURCE_CHAIN = 138;
|
||||
uint64 constant DEST_CHAIN = 5009297550715157269;
|
||||
|
||||
function setUp() public {
|
||||
mockRouter = address(new MockRouter());
|
||||
linkToken = address(new MockLinkToken());
|
||||
|
||||
sourceAggregator = new Aggregator("ETH/USD", address(this), 60, 50);
|
||||
destAggregator = new Aggregator("ETH/USD", address(this), 60, 50);
|
||||
|
||||
sender = new CCIPSender(mockRouter, address(sourceAggregator), linkToken);
|
||||
receiver = new CCIPReceiver(mockRouter, address(destAggregator));
|
||||
|
||||
sourceAggregator.addTransmitter(address(this));
|
||||
destAggregator.addTransmitter(address(receiver));
|
||||
|
||||
sender.addDestination(DEST_CHAIN, address(receiver));
|
||||
|
||||
// Mint LINK tokens to aggregator (since aggregator will pay fees)
|
||||
MockLinkToken(linkToken).mint(address(sourceAggregator), 1000e18);
|
||||
// Also mint to sender for fee calculations
|
||||
MockLinkToken(linkToken).mint(address(sender), 1000e18);
|
||||
}
|
||||
|
||||
function testCrossChainOracleSync() public {
|
||||
uint256 price = 25000000000;
|
||||
|
||||
// Update source oracle (this should trigger CCIP send if using OracleWithCCIP)
|
||||
sourceAggregator.updateAnswer(price);
|
||||
|
||||
// Approve sender to spend aggregator's LINK tokens
|
||||
vm.prank(address(sourceAggregator));
|
||||
MockLinkToken(linkToken).approve(address(sender), 1000e18);
|
||||
|
||||
// Send cross-chain update (must be called by aggregator)
|
||||
vm.prank(address(sourceAggregator));
|
||||
sender.sendOracleUpdate(DEST_CHAIN, price, 1, block.timestamp);
|
||||
|
||||
// Simulate message delivery
|
||||
IRouterClient.Any2EVMMessage memory message = IRouterClient.Any2EVMMessage({
|
||||
messageId: keccak256("test"),
|
||||
sourceChainSelector: SOURCE_CHAIN,
|
||||
sender: abi.encode(address(sender)),
|
||||
data: abi.encode(price, uint256(1), block.timestamp),
|
||||
tokenAmounts: new IRouterClient.TokenAmount[](0)
|
||||
});
|
||||
|
||||
vm.prank(mockRouter);
|
||||
receiver.ccipReceive(message);
|
||||
|
||||
// Verify destination oracle updated
|
||||
(uint256 roundId, int256 answer, , , ) = destAggregator.latestRoundData();
|
||||
assertEq(uint256(answer), price, "Destination price should match");
|
||||
assertEq(roundId, 1, "Round ID should match");
|
||||
}
|
||||
}
|
||||
|
||||
contract MockRouter is IRouterClient {
|
||||
function ccipSend(uint64, EVM2AnyMessage memory) external payable returns (bytes32, uint256) {
|
||||
return (keccak256("mock"), 0.01e18);
|
||||
}
|
||||
|
||||
function getFee(uint64, EVM2AnyMessage memory) external pure returns (uint256) {
|
||||
return 0.01e18;
|
||||
}
|
||||
|
||||
function getSupportedTokens(uint64) external pure returns (address[] memory) {
|
||||
return new address[](0);
|
||||
}
|
||||
}
|
||||
|
||||
contract MockLinkToken {
|
||||
mapping(address => uint256) public balanceOf;
|
||||
mapping(address => mapping(address => uint256)) public allowance;
|
||||
|
||||
function mint(address to, uint256 amount) external {
|
||||
balanceOf[to] += amount;
|
||||
}
|
||||
|
||||
function transfer(address to, uint256 amount) external returns (bool) {
|
||||
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
|
||||
balanceOf[msg.sender] -= amount;
|
||||
balanceOf[to] += amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
|
||||
require(balanceOf[from] >= amount, "Insufficient balance");
|
||||
require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
|
||||
balanceOf[from] -= amount;
|
||||
balanceOf[to] += amount;
|
||||
allowance[from][msg.sender] -= amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
function approve(address spender, uint256 amount) external returns (bool) {
|
||||
allowance[msg.sender][spender] = amount;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
140
test/e2e/NetworkResilience.t.sol
Normal file
140
test/e2e/NetworkResilience.t.sol
Normal file
@@ -0,0 +1,140 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {Aggregator} from "../../contracts/oracle/Aggregator.sol";
|
||||
import {CCIPSender} from "../../contracts/ccip/CCIPSender.sol";
|
||||
|
||||
contract NetworkResilienceTest is Test {
|
||||
Aggregator public aggregator;
|
||||
address public transmitter1;
|
||||
address public transmitter2;
|
||||
address public transmitter3;
|
||||
|
||||
function setUp() public {
|
||||
aggregator = new Aggregator("ETH/USD", address(this), 60, 50);
|
||||
|
||||
transmitter1 = address(0x111);
|
||||
transmitter2 = address(0x222);
|
||||
transmitter3 = address(0x333);
|
||||
|
||||
aggregator.addTransmitter(transmitter1);
|
||||
aggregator.addTransmitter(transmitter2);
|
||||
aggregator.addTransmitter(transmitter3);
|
||||
}
|
||||
|
||||
function testOracleContinuesWithOneTransmitterFailure() public {
|
||||
uint256 price1 = 25000000000;
|
||||
uint256 price2 = 25100000000;
|
||||
|
||||
// Transmitter 1 updates
|
||||
vm.prank(transmitter1);
|
||||
aggregator.updateAnswer(price1);
|
||||
|
||||
// Fast forward past heartbeat to create new round
|
||||
vm.warp(block.timestamp + 61);
|
||||
|
||||
// Transmitter 1 fails, transmitter 2 continues with new round
|
||||
vm.prank(transmitter2);
|
||||
aggregator.updateAnswer(price2);
|
||||
|
||||
(uint256 roundId, int256 answer, , , ) = aggregator.latestRoundData();
|
||||
assertEq(uint256(answer), price2, "Oracle should continue with remaining transmitters");
|
||||
assertGt(roundId, 1, "Should create new round");
|
||||
}
|
||||
|
||||
function testOracleHandlesMultipleTransmitterFailures() public {
|
||||
uint256 price1 = 25000000000;
|
||||
uint256 price2 = 25100000000;
|
||||
uint256 price3 = 25200000000;
|
||||
|
||||
// All transmitters update in round 1
|
||||
vm.prank(transmitter1);
|
||||
aggregator.updateAnswer(price1);
|
||||
|
||||
vm.prank(transmitter2);
|
||||
aggregator.updateAnswer(price1);
|
||||
|
||||
vm.prank(transmitter3);
|
||||
aggregator.updateAnswer(price1);
|
||||
|
||||
// Fast forward past heartbeat to create new round
|
||||
vm.warp(block.timestamp + 61);
|
||||
|
||||
// Two transmitters fail, one continues with new round
|
||||
vm.prank(transmitter3);
|
||||
aggregator.updateAnswer(price3);
|
||||
|
||||
(uint256 roundId, int256 answer, , , ) = aggregator.latestRoundData();
|
||||
assertEq(uint256(answer), price3, "Oracle should continue with remaining transmitter");
|
||||
assertGt(roundId, 1, "Should create new round");
|
||||
}
|
||||
|
||||
function testOracleRecoversFromPause() public {
|
||||
uint256 price1 = 25000000000;
|
||||
uint256 price2 = 25100000000;
|
||||
|
||||
// Update before pause
|
||||
vm.prank(transmitter1);
|
||||
aggregator.updateAnswer(price1);
|
||||
|
||||
// Verify initial state
|
||||
(uint256 roundId1, int256 answer1, , , ) = aggregator.latestRoundData();
|
||||
assertEq(uint256(answer1), price1, "Initial price should be set");
|
||||
|
||||
// Pause oracle (must be called by admin)
|
||||
aggregator.pause();
|
||||
|
||||
// Try to update (should fail)
|
||||
vm.prank(transmitter1);
|
||||
vm.expectRevert("Aggregator: paused");
|
||||
aggregator.updateAnswer(price2);
|
||||
|
||||
// Unpause (must be called by admin)
|
||||
aggregator.unpause();
|
||||
|
||||
// Update should work again
|
||||
vm.prank(transmitter1);
|
||||
aggregator.updateAnswer(price2);
|
||||
|
||||
(uint256 roundId2, int256 answer2, , , ) = aggregator.latestRoundData();
|
||||
assertEq(uint256(answer2), price2, "Oracle should recover after unpause");
|
||||
assertGt(roundId2, roundId1, "Should create new round after unpause");
|
||||
}
|
||||
|
||||
function testOracleHandlesStaleData() public {
|
||||
uint256 price1 = 25000000000;
|
||||
|
||||
// Initial update
|
||||
vm.prank(transmitter1);
|
||||
aggregator.updateAnswer(price1);
|
||||
|
||||
// Fast forward past heartbeat
|
||||
vm.warp(block.timestamp + 61);
|
||||
|
||||
// New update should create new round
|
||||
vm.prank(transmitter1);
|
||||
aggregator.updateAnswer(price1);
|
||||
|
||||
(uint256 roundId, , , , ) = aggregator.latestRoundData();
|
||||
assertGt(roundId, 1, "Should create new round after heartbeat");
|
||||
}
|
||||
|
||||
function testOracleHandlesPriceDeviation() public {
|
||||
uint256 price1 = 25000000000; // $250.00
|
||||
uint256 price2 = 25125000000; // $251.25 (0.5% deviation)
|
||||
|
||||
// Initial update
|
||||
vm.prank(transmitter1);
|
||||
aggregator.updateAnswer(price1);
|
||||
|
||||
// Update with deviation
|
||||
vm.prank(transmitter1);
|
||||
aggregator.updateAnswer(price2);
|
||||
|
||||
(uint256 roundId, int256 answer, , , ) = aggregator.latestRoundData();
|
||||
assertEq(uint256(answer), price2, "Oracle should accept price deviation");
|
||||
assertGt(roundId, 1, "Should create new round on deviation");
|
||||
}
|
||||
}
|
||||
|
||||
81
test/e2e/OracleFlow.t.sol
Normal file
81
test/e2e/OracleFlow.t.sol
Normal file
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {Aggregator} from "../../contracts/oracle/Aggregator.sol";
|
||||
import {OracleWithCCIP} from "../../contracts/oracle/OracleWithCCIP.sol";
|
||||
import {CCIPSender} from "../../contracts/ccip/CCIPSender.sol";
|
||||
|
||||
contract OracleFlowTest is Test {
|
||||
Aggregator public aggregator;
|
||||
OracleWithCCIP public oracleWithCCIP;
|
||||
CCIPSender public ccipSender;
|
||||
address public transmitter;
|
||||
|
||||
function setUp() public {
|
||||
transmitter = address(this);
|
||||
|
||||
aggregator = new Aggregator("ETH/USD", address(this), 60, 50);
|
||||
aggregator.addTransmitter(transmitter);
|
||||
|
||||
// Deploy CCIP components (simplified for testing)
|
||||
address mockRouter = address(0x123);
|
||||
address mockFeeToken = address(0x456);
|
||||
ccipSender = new CCIPSender(mockRouter, address(aggregator), mockFeeToken);
|
||||
|
||||
oracleWithCCIP = new OracleWithCCIP(
|
||||
"ETH/USD",
|
||||
address(this),
|
||||
60,
|
||||
50,
|
||||
address(ccipSender)
|
||||
);
|
||||
oracleWithCCIP.addTransmitter(transmitter);
|
||||
}
|
||||
|
||||
function testFullOracleUpdateFlow() public {
|
||||
uint256 price = 25000000000; // $250.00
|
||||
|
||||
// Update oracle
|
||||
oracleWithCCIP.updateAnswer(price);
|
||||
|
||||
// Verify update
|
||||
(uint256 roundId, int256 answer, , , ) = oracleWithCCIP.latestRoundData();
|
||||
assertEq(uint256(answer), price, "Price should match");
|
||||
assertEq(roundId, 1, "Round ID should be 1");
|
||||
}
|
||||
|
||||
function testOracleUpdateWithHeartbeat() public {
|
||||
uint256 price1 = 25000000000;
|
||||
uint256 price2 = 25100000000;
|
||||
|
||||
// First update
|
||||
oracleWithCCIP.updateAnswer(price1);
|
||||
|
||||
// Fast forward past heartbeat
|
||||
vm.warp(block.timestamp + 61);
|
||||
|
||||
// Second update should create new round
|
||||
oracleWithCCIP.updateAnswer(price2);
|
||||
|
||||
(uint256 roundId, int256 answer, , , ) = oracleWithCCIP.latestRoundData();
|
||||
assertEq(uint256(answer), price2, "Price should be updated");
|
||||
assertEq(roundId, 2, "Round ID should increment");
|
||||
}
|
||||
|
||||
function testOracleUpdateWithDeviation() public {
|
||||
uint256 price1 = 25000000000; // $250.00
|
||||
uint256 price2 = 25125000000; // $251.25 (0.5% deviation)
|
||||
|
||||
// First update
|
||||
oracleWithCCIP.updateAnswer(price1);
|
||||
|
||||
// Second update with deviation
|
||||
oracleWithCCIP.updateAnswer(price2);
|
||||
|
||||
(uint256 roundId, int256 answer, , , ) = oracleWithCCIP.latestRoundData();
|
||||
assertEq(uint256(answer), price2, "Price should be updated");
|
||||
assertGt(roundId, 1, "Round ID should increment on deviation");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user