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

[MD-943] - Added chainlink automation #45

Merged
merged 17 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ docs/
.history/

**/node_modules/
.vscode/

# Coverage report
report/
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ testv :; forge test -vvvv

coverage :; forge coverage --nmp test/fork/*

coverage-fork :; forge coverage --mp test/fork/*

dcoverage :; forge coverage --nmp test/fork/* --report debug > coverage.txt

hcoverage:; forge coverage --nmp test/fork/* --report lcov && genhtml lcov.info -o report --branch-coverage
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ remappings = [
'@symbiotic-collateral=lib/collateral/src/',
'@symbiotic-middleware/=lib/middleware-sdk/src/',
'@tanssi-bridge-relayer/=lib/tanssi-bridge-relayer/',
"@chainlink/=lib/chainlink-brownie-contracts/contracts/src/v0.8/",
# These are for snowbridge. Needs to emulate their remappings.
'openzeppelin-contracts/=lib/openzeppelin-contracts/',
'openzeppelin/=lib/openzeppelin-contracts/contracts/',
Expand Down
66 changes: 21 additions & 45 deletions script/DeployTanssiEcosystem.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -304,18 +304,20 @@ contract DeployTanssiEcosystem is Script {
vm.startBroadcast(ownerPrivateKey);
}

ecosystemEntities.middleware = _deployMiddlewareWithProxy(
tanssi,
operatorRegistryAddress,
vaultRegistryAddress,
operatorNetworkOptInServiceAddress,
tanssi,
NETWORK_EPOCH_DURATION,
SLASHING_WINDOW,
address(0),
operatorRewardsAddress,
stakerRewardsFactoryAddress
);
IMiddleware.InitParams memory params = IMiddleware.InitParams({
network: tanssi,
operatorRegistry: operatorRegistryAddress,
vaultRegistry: vaultRegistryAddress,
operatorNetworkOptIn: operatorNetworkOptInServiceAddress,
owner: tanssi,
epochDuration: NETWORK_EPOCH_DURATION,
slashingWindow: SLASHING_WINDOW,
reader: address(0)
});

ecosystemEntities.middleware =
_deployMiddlewareWithProxy(params, operatorRewardsAddress, stakerRewardsFactoryAddress);

networkMiddlewareService.setMiddleware(address(ecosystemEntities.middleware));
_registerEntitiesToMiddleware();

Expand All @@ -342,55 +344,29 @@ contract DeployTanssiEcosystem is Script {
}

function _deployMiddlewareWithProxy(
address network,
address operatorRegistry,
address vaultRegistry,
address operatorNetOptin,
address owner,
uint48 epochDuration,
uint48 slashingWindow,
address reader,
IMiddleware.InitParams memory params,
address operatorRewards,
address stakerRewardsFactory
) private returns (Middleware _middleware) {
Middleware _middlewareImpl = new Middleware(operatorRewards, stakerRewardsFactory);
_middleware = Middleware(address(new MiddlewareProxy(address(_middlewareImpl), "")));

if (reader == address(0)) {
reader = address(new BaseMiddlewareReader());
if (params.reader == address(0)) {
params.reader = address(new BaseMiddlewareReader());
}
_middleware.initialize(
network, operatorRegistry, vaultRegistry, operatorNetOptin, owner, epochDuration, slashingWindow, reader
);
_middleware.initialize(params);
}

function deployMiddleware(
address networkAddress,
address operatorRegistryAddress,
address vaultRegistryAddress,
address operatorNetworkOptInServiceAddress,
address ownerAddress,
uint48 epochDuration,
uint48 slashingWindow,
IMiddleware.InitParams memory params,
address operatorRewardsAddress,
address stakerRewardsFactoryAddress,
address readHelperAddress,
address networkMiddlewareServiceAddress
) external returns (address) {
vm.startBroadcast(ownerPrivateKey);

ecosystemEntities.middleware = _deployMiddlewareWithProxy(
networkAddress,
operatorRegistryAddress,
vaultRegistryAddress,
operatorNetworkOptInServiceAddress,
ownerAddress,
epochDuration,
slashingWindow,
readHelperAddress,
operatorRewardsAddress,
stakerRewardsFactoryAddress
);
ecosystemEntities.middleware =
_deployMiddlewareWithProxy(params, operatorRewardsAddress, stakerRewardsFactoryAddress);

if (networkMiddlewareServiceAddress != address(0)) {
INetworkMiddlewareService(networkMiddlewareServiceAddress).setMiddleware(
Expand Down
134 changes: 110 additions & 24 deletions src/contracts/middleware/Middleware.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pragma solidity 0.8.25;
//**************************************************************************************************
// CHAINLINK
//**************************************************************************************************
import {AutomationCompatibleInterface} from "@chainlink/automation/interfaces/AutomationCompatibleInterface.sol";
import {AggregatorV3Interface} from "@chainlink/shared/interfaces/AggregatorV2V3Interface.sol";

//**************************************************************************************************
Expand Down Expand Up @@ -63,6 +64,7 @@ contract Middleware is
KeyManager256,
OzAccessControl,
EpochCapture,
AutomationCompatibleInterface,
MiddlewareStorage,
IMiddleware
{
Expand Down Expand Up @@ -106,32 +108,34 @@ contract Middleware is
* @param reader The reader address
*/
function initialize(
address network,
address operatorRegistry,
address vaultRegistry,
address operatorNetOptin,
address owner,
uint48 epochDuration,
uint48 slashingWindow,
address reader
) public initializer {
{
_checkNotZeroAddress(owner);
_checkNotZeroAddress(reader);
InitParams memory params
) public initializer notZeroAddress(params.owner) notZeroAddress(params.reader) {
if (params.slashingWindow < params.epochDuration) {
revert Middleware__SlashingWindowTooShort();
}

if (slashingWindow < epochDuration) {
revert Middleware__SlashingWindowTooShort();
{
StorageMiddleware storage $ = _getMiddlewareStorage();
$.lastTimestamp = Time.timestamp();
$.interval = params.epochDuration;
}

__BaseMiddleware_init(network, slashingWindow, vaultRegistry, operatorRegistry, operatorNetOptin, reader);
__OzAccessControl_init(owner);
__EpochCapture_init(epochDuration);
__BaseMiddleware_init(
params.network,
params.slashingWindow,
params.vaultRegistry,
params.operatorRegistry,
params.operatorNetworkOptIn,
params.reader
);
__OzAccessControl_init(params.owner);
__EpochCapture_init(params.epochDuration);
__UUPSUpgradeable_init();

_grantRole(DEFAULT_ADMIN_ROLE, owner);
_grantRole(DEFAULT_ADMIN_ROLE, params.owner);
_setSelectorRole(this.distributeRewards.selector, GATEWAY_ROLE);
_setSelectorRole(this.slash.selector, GATEWAY_ROLE);
_setSelectorRole(this.performUpkeep.selector, FORWARDER_ROLE);
}

function _authorizeUpgrade(
Expand All @@ -144,7 +148,7 @@ contract Middleware is
function _afterRegisterSharedVault(
address sharedVault,
IODefaultStakerRewards.InitParams memory stakerRewardsParams
) internal virtual override {
) internal override {
address stakerRewards =
IODefaultStakerRewardsFactory(i_stakerRewardsFactory).create(sharedVault, stakerRewardsParams);

Expand Down Expand Up @@ -175,12 +179,52 @@ contract Middleware is
* @inheritdoc IMiddleware
*/
function setGateway(
address _gateway
address newGateway
) external checkAccess notZeroAddress(newGateway) {
StorageMiddleware storage $ = _getMiddlewareStorage();
address oldGateway = $.gateway;

if (newGateway == oldGateway) {
revert Middleware__AlreadySet();
}

$.gateway = newGateway;
_revokeRole(GATEWAY_ROLE, oldGateway);
_grantRole(GATEWAY_ROLE, newGateway);
}

/**
* @inheritdoc IMiddleware
*/
function setInterval(
uint256 interval
) external checkAccess {
if (interval == 0) {
revert Middleware__InvalidInterval();
}
StorageMiddleware storage $ = _getMiddlewareStorage();
_revokeRole(GATEWAY_ROLE, $.gateway);
$.gateway = _gateway;
_grantRole(GATEWAY_ROLE, _gateway);

if (interval == $.interval) {
revert Middleware__AlreadySet();
}

$.interval = interval;
}

/**
* @inheritdoc IMiddleware
*/
function setForwarder(
address forwarder
) external checkAccess notZeroAddress(forwarder) {
StorageMiddleware storage $ = _getMiddlewareStorage();

if (forwarder == $.forwarderAddress) {
revert Middleware__AlreadySet();
}

$.forwarderAddress = forwarder;
_grantRole(FORWARDER_ROLE, forwarder);
}

function setCollateralToOracle(
Expand Down Expand Up @@ -237,10 +281,52 @@ contract Middleware is

uint48 epoch = getCurrentEpoch();
sortedKeys = sortOperatorsByVaults(epoch);

IOGateway(gateway).sendOperatorsData(sortedKeys, epoch);
}

/**
* @inheritdoc AutomationCompatibleInterface
* @dev Called by chainlink nodes off-chain to check if the upkeep is needed
* @return upkeepNeeded boolean to indicate whether the keeper should call performUpkeep or not.
* @return performData bytes of the sorted operators' keys and the epoch that will be used by the keeper when calling performUpkeep, if upkeep is needed.
*/
function checkUpkeep(
bytes calldata /* checkData */
) external view override returns (bool upkeepNeeded, bytes memory performData) {
uint48 epoch = getCurrentEpoch();
bytes32[] memory sortedKeys = sortOperatorsByVaults(epoch);
StorageMiddleware storage $ = _getMiddlewareStorage();

//TODO Should it be epochDuration? Because we wanna send it once per network epoch
upkeepNeeded = (Time.timestamp() - $.lastTimestamp) > $.interval;

performData = abi.encode(sortedKeys, epoch);
}

/**
* @inheritdoc AutomationCompatibleInterface
* @dev Called by chainlink nodes off-chain to perform the upkeep. It will send the sorted keys to the gateway
*/
function performUpkeep(
bytes calldata performData
) external override checkAccess {
StorageMiddleware storage $ = _getMiddlewareStorage();
address gateway = $.gateway;
if (gateway == address(0)) {
revert Middleware__GatewayNotSet();
}

uint48 currentTimestamp = Time.timestamp();
if ((currentTimestamp - $.lastTimestamp) > $.interval) {
$.lastTimestamp = currentTimestamp;

// Decode the sorted keys and the epoch from performData
(bytes32[] memory sortedKeys, uint48 epoch) = abi.decode(performData, (bytes32[], uint48));

IOGateway(gateway).sendOperatorsData(sortedKeys, epoch);
}
}

/**
* @inheritdoc IMiddleware
*/
Expand Down
43 changes: 43 additions & 0 deletions src/contracts/middleware/MiddlewareStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ abstract contract MiddlewareStorage {
/// @custom:storage-location erc7201:tanssi.middleware.MiddlewareStorage.v1.1
struct StorageMiddleware {
address gateway;
uint256 lastTimestamp;
uint256 interval;
address forwarderAddress;
mapping(address collateral => address oracle) collateralToOracle;
mapping(address vault => address collateral) vaultToCollateral;
}
Expand All @@ -31,6 +34,7 @@ abstract contract MiddlewareStorage {
uint256 public constant VERSION = 1;
uint256 public constant PARTS_PER_BILLION = 1_000_000_000;
bytes32 internal constant GATEWAY_ROLE = keccak256("GATEWAY_ROLE");
bytes32 internal constant FORWARDER_ROLE = keccak256("FORWARDER_ROLE");

/**
* @notice Get the operator rewards contract address
Expand Down Expand Up @@ -59,20 +63,59 @@ abstract contract MiddlewareStorage {
return $.gateway;
}

/**
* @notice Get the last timestamp
* @return last timestamp
*/
function getLastTimestamp() public view returns (uint256) {
StorageMiddleware storage $ = _getMiddlewareStorage();
return $.lastTimestamp;
}

/**
* @notice Get the forwarder address
* @return forwarder address
*/
function getForwarderAddress() public view returns (address) {
StorageMiddleware storage $ = _getMiddlewareStorage();
return $.forwarderAddress;
}

/**
* @notice Get the interval
* @return interval
*/
function getInterval() public view returns (uint256) {
StorageMiddleware storage $ = _getMiddlewareStorage();
return $.interval;
}

/**
* @notice Get the oracle address for a collateral
* @return oracle address
*/
function collateralToOracle(
address collateral
) public view returns (address) {
StorageMiddleware storage $ = _getMiddlewareStorage();
return $.collateralToOracle[collateral];
}

/**
* @notice Get the collateral address for a vault
* @return collateral address
*/
function vaultToCollateral(
address vault
) public view returns (address) {
StorageMiddleware storage $ = _getMiddlewareStorage();
return $.vaultToCollateral[vault];
}

/**
* @notice Get the oracle address for a vault
* @return oracle address
*/
function vaultToOracle(
address vault
) public view returns (address) {
Expand Down
Loading
Loading