DEV Community

Cover image for Deploying smartcontracts through github actions using AWS secret manager
Rodrigo Burgos
Rodrigo Burgos

Posted on

Deploying smartcontracts through github actions using AWS secret manager

Imagine that you want to deploy a SC within deployments on github without compromising your private and api keys? This is accessable through github actions.

PROS

  • Security
  • Easy to work with teams
  • Apply test cases before deployments

CON

  • Hard to verify

First let's use a simple token lock as example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
}

contract TokenLockVault {
    struct Lock {
        uint256 amount;
        uint256 unlockTime;
    }

    mapping(address => mapping(address => Lock)) public locks;

    event TokensLocked(address indexed user, address indexed token, uint256 amount, uint256 unlockTime);
    event TokensWithdrawn(address indexed user, address indexed token, uint256 amount);

    function lockTokens(address token, uint256 amount, uint256 duration) external {
        if (amount == 0) revert("Amount cannot be zero");
        if (duration == 0) revert("Duration must be greater than zero");

        uint256 unlockTime = block.timestamp + duration;
        locks[msg.sender][token] = Lock(amount, unlockTime);

        bool success = IERC20(token).transferFrom(msg.sender, address(this), amount);
        if (!success) revert("Token transfer failed");

        emit TokensLocked(msg.sender, token, amount, unlockTime);
    }

    function withdrawTokens(address token) external {
        Lock memory userLock = locks[msg.sender][token];
        address locker = msg.sender;

        if (userLock.amount == 0) revert("No tokens locked");
        if (block.timestamp < userLock.unlockTime) revert("Tokens are still locked");

        delete locks[locker][token];

        bool success = IERC20(token).transfer(locker, userLock.amount);
        if (!success) revert("Token transfer failed");

        emit TokensWithdrawn(msg.sender, token, userLock.amount);
    }
}

Enter fullscreen mode Exit fullscreen mode

With this test case scenario:

import hre from "hardhat";
import { token, TokenLockVault } from "../typechain-types";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import { time } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { expect } from "chai";

describe("Testing token lock", function () {
  let tokenLocker: TokenLockVault;
  let ethus: Token;
  let owner: HardhatEthersSigner, locker: HardhatEthersSigner

  beforeEach(async () => {
    [owner, locker ] = await hre.ethers.getSigners();

    tokenLocker = await hre.ethers.deployContract("TokenLockVault") as EthusTokenLockVault;
    ethus = await hre.ethers.deployContract("token") as token;

    console.log("Deploying contracts...");

    // Ensure contracts are initialized correctly
    await token.connect(owner).initialize(owner.address);

    console.log("Contracts deployed and initialized successfully");

    // Transfer tokens to liquidity providers
    const balance0 = await token.balanceOf(owner.address);
    await token.transfer(locker.address, balance0 / BigInt(2));
  });

  it("Should lock tokens", async () => {
    const balance = await ethus.balanceOf(locker.address);
    const TEN_DAYS_IN_SECONDS = 10 * 24 * 60 * 60;
    const unlockTime = (await time.latest()) + TEN_DAYS_IN_SECONDS;

    await ethus.connect(locker).approve(await tokenLocker.getAddress(), balance);
    await tokenLocker.connect(locker).lockTokens(await ethus.getAddress(), balance, TEN_DAYS_IN_SECONDS);

    const lock = await tokenLocker.connect(locker).locks(locker.address, ethus.getAddress());

    expect(lock[0]).to.equal(balance);
    expect(lock[1]).to.be.closeTo(unlockTime, 2);
  })

  it("should revert if we tray to unlock tokens before the unlock time", async () => {
    const balance = await token.balanceOf(locker.address);
    const TEN_DAYS_IN_SECONDS = 10 * 24 * 60 * 60;

    await token.connect(locker).approve(await tokenLocker.getAddress(), balance);
    await tokenLocker.connect(locker).lockTokens(await ethus.getAddress(), balance, TEN_DAYS_IN_SECONDS);

    await expect(tokenLocker.connect(locker).withdrawTokens(await ethus.getAddress())).to.be.revertedWith("Tokens are still locked");
  })

  it("Should allow withdrawner to withdraw tokens after the unlock time", async () => {
    const balance = await token.balanceOf(locker.address);
    const TEN_DAYS_IN_SECONDS = 10 * 24 * 60 * 60;
    const unlockTime = (await time.latest()) + TEN_DAYS_IN_SECONDS;

    await token.connect(locker).approve(await tokenLocker.getAddress(), balance);
    await tokenLocker.connect(locker).lockTokens(await ethus.getAddress(), balance, TEN_DAYS_IN_SECONDS);

    await time.increase(TEN_DAYS_IN_SECONDS + 1);
    await tokenLocker.connect(locker).withdrawTokens(await ethus.getAddress());

    const lock = await tokenLocker.connect(locker).locks(locker.address, ethus.getAddress());
    expect(lock[0]).to.equal(0);
    expect(lock[1]).to.equal(0);
  })
});
Enter fullscreen mode Exit fullscreen mode

Let's breakdown the github workflow YML:

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Set up AWS CLI
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.ACCESS_KEY }}
          aws-secret-access-key: ${{ secrets.SECRET_KEY }}
          aws-region: us-east-1

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "22" # Ensure this matches your local setup

      - name: Install dependencies
        run: npm install --legacy-peer-deps

      - name: Retrieve PRIVATE_KEY from AWS Secrets Manager
        uses: aws-actions/aws-secretsmanager-get-secrets@v2
        with:
          secret-ids: PRIVATE_KEY
          parse-json-secrets: false
      - name: Set Hardhat Vars Before Tests
        run: |
          echo "PRIVATE_KEY=$PRIVATE_KEY" >> $GITHUB_ENV  # Set PRIVATE_KEY as an environment variable
          npx hardhat vars set PRIVATE_KEY $PRIVATE_KEY  # Directly pass the value of PRIVATE_KEY
          npx hardhat vars set ALCHEMY_POLYGON_MAINNET_KEY ${{ secrets.ALCHEMY_POLYGON_MAINNET_KEY }}
          npx hardhat vars set POLYGONSCAN_API_KEY ${{ secrets.POLYGONSCAN_API_KEY }}

      - name: Run Hardhat Tests
        run: npx hardhat test
Enter fullscreen mode Exit fullscreen mode

In this part we extract the private key from the deployer wallet from aws secret manager.

eploy:
    name: Deploy Smart Contract
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/locker'

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Set up AWS CLI
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.ACCESS_KEY }}
          aws-secret-access-key: ${{ secrets.SECRET_KEY }}
          aws-region: us-east-1

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "22"

      - name: Install dependencies
        run: npm install --legacy-peer-deps

      - name: Retrieve PRIVATE_KEY from AWS Secrets Manager
        uses: aws-actions/aws-secretsmanager-get-secrets@v2
        with:
          secret-ids: PRIVATE_KEY
          parse-json-secrets: false

      - name: Set Hardhat Vars Before Deploy
        run: |
          echo "PRIVATE_KEY=$PRIVATE_KEY" >> $GITHUB_ENV  # Set PRIVATE_KEY as an environment variable
          npx hardhat vars set PRIVATE_KEY $PRIVATE_KEY
          npx hardhat vars set ALCHEMY_POLYGON_MAINNET_KEY ${{ secrets.ALCHEMY_POLYGON_MAINNET_KEY }}
          npx hardhat vars set POLYGONSCAN_API_KEY ${{ secrets.POLYGONSCAN_API_KEY }}

      - name: Deploy Smart Contract
        run: echo "y" | npx hardhat ignition deploy ignition/modules/Lock.ts --network polygon --verify

      - name: Verify Deployed Smart Contract
        run: |
          npx hardhat ignition verify chain-137 --include-unrelated-contracts

Enter fullscreen mode Exit fullscreen mode

Right after we use vars set from hardhat to set variables extracted from both github secrets and AWS secret manager the hardhat enviroment and proceed to deploy and verfify the smartcontracts.

My last comments, i really don't think this is the best approach to handle public smartcontracts because it's hard to verify. At deployment time the scan didn't indexed the smartcontract.

But it's okey to corporative, testnet or depending to the usecase of your smartcontract.

Top comments (0)