diff --git a/.github/workflows/npm.yaml b/.github/workflows/npm.yaml index fa4f33e..05653ce 100644 --- a/.github/workflows/npm.yaml +++ b/.github/workflows/npm.yaml @@ -19,6 +19,10 @@ on: - latest - beta +permissions: + id-token: write # Required for OIDC + contents: read + jobs: npm-upload: name: NPM Package Build and Upload @@ -29,10 +33,15 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" + registry-url: 'https://registry.npmjs.org' + + # Ensure npm 11.5.1 or later is installed + - name: Update npm + run: npm install -g npm@latest - run: npm ci @@ -47,8 +56,6 @@ jobs: - run: scripts/make-npm-package.sh "${{ steps.semver.outputs.semver }}" ./build/npm-package - - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTOMATION_TOKEN }}" > ./build/npm-package/.npmrc - - run: | RELEASE_TAG="${{ github.event.inputs.tag }}" if [ -z "$RELEASE_TAG" ]; then diff --git a/contracts/TestCurrencyPermit.sol b/contracts/TestCurrencyPermit.sol new file mode 100644 index 0000000..0f3f7e4 --- /dev/null +++ b/contracts/TestCurrencyPermit.sol @@ -0,0 +1,31 @@ +//SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +contract TestCurrencyPermit is ERC20Permit { + uint8 internal immutable _decimals; + + constructor( + string memory name_, + string memory symbol_, + uint256 initialSupply, + uint8 decimals_ + ) ERC20(name_, symbol_) ERC20Permit(name_) { + _decimals = decimals_; + _mint(msg.sender, initialSupply); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + function mint(address recipient, uint256 amount) public virtual { + return _mint(recipient, amount); + } + + function burn(address recipient, uint256 amount) public virtual { + return _burn(recipient, amount); + } +} diff --git a/js/utils.js b/js/utils.js index 0ad2672..a524416 100644 --- a/js/utils.js +++ b/js/utils.js @@ -2,7 +2,8 @@ const { findAll } = require("solidity-ast/utils"); const ethers = require("ethers"); const withArgsInternal = require("@nomicfoundation/hardhat-chai-matchers/internal/withArgs"); -const { IMPLEMENTATION_SLOT } = require("./constants"); +const helpers = require("@nomicfoundation/hardhat-network-helpers"); +const { IMPLEMENTATION_SLOT, HOUR } = require("./constants"); const _E = ethers.parseEther; const WAD = 10n ** 18n; // 1e18 @@ -351,6 +352,69 @@ function newCaptureAny() { const captureAny = newCaptureAny(); +async function makeEIP2612Signature(hre, token, owner, spenderAddress, value, deadline = HOUR) { + // From: https://www.quicknode.com/guides/ethereum-development/transactions/how-to-use-erc20-permit-approval + const chainId = hre.network.config.chainId; + // set the domain parameters + const tokenAddr = await ethers.resolveAddress(token); + const domain = { + name: await token.name(), + version: "1", + chainId: chainId, + verifyingContract: tokenAddr, + }; + + // set the Permit type parameters + const types = { + Permit: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + { + name: "nonce", + type: "uint256", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }; + + if (deadline < 1600000000) { + // Is a duration in seconds + deadline = (await helpers.time.latest()) + deadline; + } + + const nonces = await token.nonces(owner); + + // set the Permit type values + const ownerAddr = await ethers.resolveAddress(owner); + const values = { + owner: ownerAddr, + spender: spenderAddress, + value: value, + nonce: nonces, + deadline: deadline, + }; + + // sign the Permit type data with the deployer's private key + const signature = await owner.signTypedData(domain, types, values); + + // split the signature into its components + const sig = ethers.Signature.from(signature); + return { sig, deadline, nonces }; +} + module.exports = { _E, _A, @@ -381,4 +445,5 @@ module.exports = { captureAny, newCaptureAny, AM_ROLES, + makeEIP2612Signature, }; diff --git a/test/test-utils-functions.js b/test/test-utils-functions.js index 7208886..75268ad 100644 --- a/test/test-utils-functions.js +++ b/test/test-utils-functions.js @@ -1,7 +1,7 @@ const hre = require("hardhat"); const { expect } = require("chai"); const helpers = require("@nomicfoundation/hardhat-network-helpers"); -const { _A, getRole, grantRole } = require("../js/utils"); +const { _A, getRole, grantRole, makeEIP2612Signature } = require("../js/utils"); const { initCurrency } = require("../js/test-utils"); const { ethers } = hre; @@ -36,6 +36,23 @@ describe("Utils library tests", function () { return { currency }; } + async function deployFixturePermit() { + // Fixture with TestCurrency (without access control) + const currency = await initCurrency( + { + name: "Test USDC", + symbol: "USDC", + decimals: 6, + initial_supply: _A(50000), + contractClass: "TestCurrencyPermit", + }, + [anon, user1, user2, admin], + [_A("10000"), _A("2000"), _A("1000"), _A("20000")] + ); + + return { currency }; + } + it("Checks only MINTER_ROLE can mint (TestCurrencyAC)", async () => { const { currency } = await helpers.loadFixture(deployACFixture); @@ -63,6 +80,31 @@ describe("Utils library tests", function () { expect(await currency.balanceOf(anon)).to.equal(_A(9950)); }); + it("Checks gasless spending approvals (TestCurrencyPermit)", async () => { + const { currency } = await helpers.loadFixture(deployFixturePermit); + + expect(await currency.balanceOf(user1)).to.equal(_A(2000)); + expect(await currency.balanceOf(user2)).to.equal(_A(1000)); + + const { sig, deadline } = await makeEIP2612Signature( + hre, + currency, + user1, + await ethers.resolveAddress(user2), + _A(200) + ); + await expect(currency.permit(user1, user2, _A(200), deadline, sig.v, sig.r, sig.s)) + .to.emit(currency, "Approval") + .withArgs(user1, user2, _A(200)); + + await expect(currency.connect(user2).transferFrom(user1, user2, _A(60))) + .to.emit(currency, "Transfer") + .withArgs(user1, user2, _A(60)); + + expect(await currency.balanceOf(user1)).to.equal(_A(2000 - 60)); + expect(await currency.balanceOf(user2)).to.equal(_A(1000 + 60)); + }); + it("Checks TestERC4626", async () => { const { currency } = await helpers.loadFixture(deployACFixture);