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

activates PROD deploy protection through github actions #761

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ GOERLI_ETHERSCAN_API_KEY=
LINEATEST_ETHERSCAN_API_KEY=
MUMBAI_ETHERSCAN_API_KEY=
SEPOLIA_ETHERSCAN_API_KEY=

# Github API
$GITHUB_TOKEN # this is required for Git CLI (only required for advanced scripts)
78 changes: 78 additions & 0 deletions .github/workflows/protectAuditCompletedLabel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# - Protect "AuditCompleted" Label
# - makes sure that the label "AuditCompleted" can only be assigned by a Github action and not by a human actor
# - will undo any unauthorized change of this label
# - will fail if it runs into an error, otherwise pass

name: Protect "AuditCompleted" Label

on:
pull_request: #### << needs to be changed to 'pull_request' to activate it
types: [labeled, unlabeled]

jobs:
protect_audit_label:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Log Event Payload
run: |
if [[ "${{ github.actor }}" == "github-actions" ]]; then
echo "This action was triggered by another GitHub Action."
else
echo "This action was triggered by a user: ${{ github.actor }}."
fi
echo "${{ github.event }}"

- name: Check if "AuditCompleted" label was modified
env:
GITHUB_TOKEN: ${{ secrets.LIFI_GIT_ACTIONS_TOKEN }}
GH_PAT: ${{ secrets.LIFI_GIT_ACTIONS_TOKEN }}
run: |
# The label being monitored
TARGET_LABEL="AuditCompleted"

# Check if the event was triggered by any other github action
if [[ "${{ github.actor }}" != "lifiGitActions" ]]; then #### TODO: REPLACE WITH GITHUB_ACTIONS_PAT and USERNAME <<<<<-----------
echo "This event was triggered by ${{ github.actor }}. Checking label..."

# Determine if the label was added or removed
ACTION_TYPE="none"
if [[ "${{ github.event.action }}" == "labeled" && "${{ github.event.label.name }}" == "$TARGET_LABEL" ]]; then
ACTION_TYPE="added"
elif [[ "${{ github.event.action }}" == "unlabeled" && "${{ github.event.label.name }}" == "$TARGET_LABEL" ]]; then
ACTION_TYPE="removed"
fi

# Revert the label change if necessary
if [[ "$ACTION_TYPE" != "none" ]]; then
echo -e "\033[31mUnauthorized modification of '$TARGET_LABEL' by ${{ github.actor }}. Reverting change...\033[0m"

##### remove or re-add label, depending on the case
if [[ "$ACTION_TYPE" == "added" ]]; then
# Remove the unauthorized label addition
gh pr edit ${{ github.event.pull_request.number }} --remove-label "$TARGET_LABEL"
elif [[ "$ACTION_TYPE" == "removed" ]]; then
# Re-add the unauthorized label removal
gh pr edit ${{ github.event.pull_request.number }} --add-label "$TARGET_LABEL"
fi

# make sure that the label change was undone
CURRENT_LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name')
if [[ "$ACTION_TYPE" == "added" && "$CURRENT_LABELS" == *"$TARGET_LABEL"* ]]; then
echo -e "\033[31Failed to remove the unauthorized 'AuditCompleted' label.\033[0m"
exit 1
elif [[ "$ACTION_TYPE" == "removed" && "$CURRENT_LABELS" != *"$TARGET_LABEL"* ]]; then
echo -e "\033[31Failed to re-add the 'AuditCompleted' label.\033[0m"
exit 1
fi

echo -e "\033[32Unauthorized label modification was successfully prevented and undone.\033[0m"
else
echo -e "\033[32mNo unauthorized modifications detected.\033[0m"
fi
else
echo -e "\033[32mLabel change initiated by GitHub Action. No checks required.\033[0m"
fi
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ Sample requests to fetch transactions for each facet can be found at the end of
## Getting Started<a name="getting-started"></a>

Make sure to copy `.env.example` to `.env` and fill out the missing values. Tests might fail with missing environment variables if some of the variables are blank.
Some advanced scripts require the Github CLI to be installed. Check out <a href="https://cli.github.com">this link </a> for Git CLI installation instructions (`brew install gh` on Mac)

### INSTALL<a name="install"></a>

Expand Down
2 changes: 1 addition & 1 deletion script/deploy/facets/utils/ScriptBase.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { Script, console } from "forge-std/Script.sol";
import { Script, console2, console } from "forge-std/Script.sol";
import { DSTest } from "ds-test/test.sol";

contract ScriptBase is Script, DSTest {
Expand Down
12 changes: 11 additions & 1 deletion script/deploy/facets/utils/UpdateScriptBase.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { ScriptBase, console } from "./ScriptBase.sol";
import { ScriptBase, console2, console } from "./ScriptBase.sol";
import { stdJson } from "forge-std/StdJson.sol";
import { DiamondCutFacet, IDiamondCut } from "lifi/Facets/DiamondCutFacet.sol";
import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol";
Expand Down Expand Up @@ -61,6 +61,16 @@ contract UpdateScriptBase is ScriptBase {

buildDiamondCut(getSelectors(name, excludes), facet);

// for deployments to PROD (= empty fileSuffix), make sure that the contract is audited before creating the diamondCut
if (keccak256(abi.encode(fileSuffix)) == keccak256(abi.encode(""))) {
//
string[] memory cmd = new string[](1);
cmd[0] = "script/tasks/verifyProdDeployment.sh";
bytes memory res = vm.ffi(cmd);
console2.log("Response of 'verifyProdDeployment' script:");
console2.logBytes(res);
}

if (noBroadcast) {
if (cut.length > 0) {
cutData = abi.encodeWithSelector(
Expand Down
146 changes: 146 additions & 0 deletions script/tasks/verifyProdDeployment.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/bin/bash

verifyProdDeployment() {
# load required resources
source .env
source script/config.sh
source script/helperFunctions.sh

echo "---------------------------"
echo "Verifying PROD Deployment now..."


# Contract name is passed as an argument to the script
CONTRACT_NAME=$1
if [ -z "$CONTRACT_NAME" ]; then
echo -e "\033[31mERROR: No contract name provided. Deployment is not allowed.\033[0m"
exit 1
fi

FILE_PATH=$(getContractFilePath "$CONTRACT_NAME")

# check if the file (path) was found
if [[ $? -ne 0 ]]; then
echo -e "\033[31mERROR: Could not find file path for contract $CONTRACT_NAME. Are you in the right branch? Cannot continue.\033[0m"
exit 1
fi

# Extract the contract version from the contract
CONTRACT_VERSION=$(grep -E '^/// @custom:version' "$FILE_PATH" | awk '{print $3}' || true)

# throw error if no version was found
if [ -z "$CONTRACT_VERSION" ]; then
echo -e "\033[31mERROR: Could not determine the contract version of $FILE_PATH. Deployment is not allowed.\033[0m"
exit 1
fi

echo "Contract: $CONTRACT_NAME (Version: $CONTRACT_VERSION)"

# Check if the contract in this very version exists in the main branch
MAIN_BRANCH="main"
git fetch origin $MAIN_BRANCH &>/dev/null
if git cat-file -e "origin/$MAIN_BRANCH:$FILE_PATH" &>/dev/null; then
# get the version of the same contract in main branch
MAIN_CONTRACT_VERSION=$(git show origin/$MAIN_BRANCH:$FILE_PATH | grep -E '^/// @custom:version' | awk '{print $3}' || true)

echo "MAIN_CONTRACT_VERSION: $MAIN_CONTRACT_VERSION"

# check if the contract versions are equal
if [ "$CONTRACT_VERSION" = "$MAIN_CONTRACT_VERSION" ]; then
echo -e "\033[32mContract $CONTRACT_NAME (v$CONTRACT_VERSION) found in main branch. Deployment is allowed.\033[0m"
exit 0
else
echo -e "\033[33mContract $CONTRACT_NAME's version in main branch ($MAIN_CONTRACT_VERSION) differs from to-be-deployed version ($CONTRACT_VERSION).\033[0m"
fi
else
echo -e "\033[33mContract $CONTRACT_NAME does not exist in main branch.\033[0m"
fi

# Get the current branch name
CURRENT_BRANCH=$(git branch --show-current)
echo "Checking now if open PRs exist for the current branch $CURRENT_BRANCH that contain any audit information..."

# Fetch all PRs for this branch from GitHub
ORG_NAME="lifinance"
REPO_NAME="contracts"

echo "$GITHUB_TOKEN" | gh auth login --with-token

# Check if authentication was successful
if ! gh auth status &>/dev/null; then
echo "GitHub CLI authentication failed. Please check your GITHUB_TOKEN in .env."
exit 1
fi

# Use GitHub CLI to get PRs associated with the current branch
PRs=$(gh pr list --repo "$ORG_NAME/$REPO_NAME" --head "$CURRENT_BRANCH" --json number,title,labels,baseRefName)

# Check the "AuditCompleted" label for each PR
FAILED=0

# If no PRs found, output an error and exit
if [ -z "$PRs" ]; then
echo -e "\033[31mERROR: No PRs found for branch $CURRENT_BRANCH. Deployment is not allowed.\033[0m"
exit 1
fi

# Go through all PRs
for row in $(echo "${PRs}" | jq -r '.[] | @base64'); do
# Function to extract PR parameters
_extractPRParameter() {
echo ${row} | base64 --decode | jq -r ${1}
}

PR_NUMBER=$(_extractPRParameter '.number')
PR_TITLE=$(_extractPRParameter '.title')
PR_LABELS=$(_extractPRParameter '.labels[].name')

# we need to make sure that a label cannot be quickly/temporarily manually assigned (before being removed by git action) to bypass this check
# therefore we make sure that no actions are running or queued before checking the PR itself
# Fetch the head SHA of the PR
PR_INFO=$(gh pr view "$PR_NUMBER" --repo "$ORG_NAME/$REPO_NAME" --json headRefOid)
PR_SHA=$(echo "$PR_INFO" | jq -r '.headRefOid')

echo "PR_SHA: $PR_SHA"

# Fetch workflow runs for the specific commit SHA
WORKFLOW_RUNS=$(gh api "repos/$ORG_NAME/$REPO_NAME/actions/runs?head_sha=$PR_SHA" --paginate)

# Check if there was an error fetching the runs
if [ $? -ne 0 ]; then
echo -e "\033[31mError fetching workflow runs: $WORKFLOW_RUNS for commit $PR_SHA in PR $PR_NUMBER\033[0m"
exit 1
fi

# Check for running or queued workflows
RUNNING_OR_QUEUED=$(echo "$WORKFLOW_RUNS" | jq -r '.workflow_runs[] | select(.status == "in_progress" or .status == "queued") | "\(.name) - Status: \(.status) - Event: \(.event)"')
# echo "RUNNING_OR_QUEUED: $RUNNING_OR_QUEUED"
if [ ! -z "$RUNNING_OR_QUEUED" ]; then
echo -e "\033[31mThere are running or queued github actions for PR #$PR_NUMBER:\033[0m"
echo -e "\033[33m$RUNNING_OR_QUEUED\033[0m"
echo -e "\033[31mWe cannot safely verify PROD deployment while Github actions are still running. Please wait until they are finished and try again.\033[0m"
exit 1
fi

# Check if "AuditCompleted" label is present
if echo "$PR_LABELS" | grep -wq "AuditCompleted"; then
echo -e "\033[32mPR #$PR_NUMBER ($PR_TITLE) is audited and ready for deployment.\033[0m"
else
echo -e "\033[31mPR #$PR_NUMBER ($PR_TITLE) is NOT audited. Deployment is not allowed.\033[0m"
FAILED=1
fi
done

# Final decision based on checks
if [ $FAILED -eq 1 ]; then
echo -e "\033[31mERROR: One or more PRs do not have the 'AuditCompleted' label. Deployment is not allowed.\033[0m"
exit 1
else
echo -e "\033[32mAll PRs are audited. Proceeding with deployment.\033[0m"
fi

echo "---------------------------"
}


verifyProdDeployment "NewFacet"
52 changes: 52 additions & 0 deletions src/Facets/NewFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import { LibDiamond } from "../Libraries/LibDiamond.sol";
import { LibAccess } from "../Libraries/LibAccess.sol";
import { CannotAuthoriseSelf } from "../Errors/GenericErrors.sol";

/// @title Access Manager Facet
/// @author LI.FI (https://li.fi)
/// @notice Provides functionality for managing method level access control
/// @custom:version 1.0.0
contract AccessManagerFacet {
/// Events ///

event ExecutionAllowed(address indexed account, bytes4 indexed method);
event ExecutionDenied(address indexed account, bytes4 indexed method);

/// External Methods ///

/// @notice Sets whether a specific address can call a method
/// @param _selector The method selector to set access for
/// @param _executor The address to set method access for
/// @param _canExecute Whether or not the address can execute the specified method
function setCanExecute(
bytes4 _selector,
address _executor,
bool _canExecute
) external {
if (_executor == address(this)) {
revert CannotAuthoriseSelf();
}
LibDiamond.enforceIsContractOwner();
_canExecute
? LibAccess.addAccess(_selector, _executor)
: LibAccess.removeAccess(_selector, _executor);
if (_canExecute) {
emit ExecutionAllowed(_executor, _selector);
} else {
emit ExecutionDenied(_executor, _selector);
}
}

/// @notice Check if a method can be executed by a specific address
/// @param _selector The method selector to check
/// @param _executor The address to check
function addressCanExecuteMethod(
bytes4 _selector,
address _executor
) external view returns (bool) {
return LibAccess.accessStorage().execAccess[_selector][_executor];
}
}
Loading