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

Remove dependency on solidity-stringutils #91

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: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/solidity-stringutils"]
path = lib/solidity-stringutils
url = https://github.com/Arachnid/solidity-stringutils
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Remove dependency on `solidity-stringutils`. ([#91](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/pull/91))

## 0.3.8 (2025-01-24)

- Fix error conditions when warnings occur in validation output. ([#94](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/pull/94))
Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,18 @@ Follow the steps above, but instead of running `forge install OpenZeppelin/openz
npm install @openzeppelin/foundry-upgrades
```

Then add the following additional lines to `remappings.txt`, in addition to the ones described above:
Then add the following additional line to `remappings.txt`, in addition to the ones described above:
```
openzeppelin-foundry-upgrades/=node_modules/@openzeppelin/foundry-upgrades/src/
solidity-stringutils/=node_modules/@openzeppelin/foundry-upgrades/lib/solidity-stringutils/
```

#### Soldeer

Follow the steps above, but instead of running `forge install OpenZeppelin/openzeppelin-foundry-upgrades`, use one of the install commands described in https://soldeer.xyz/project/openzeppelin-foundry-upgrades

Then add the following additional lines to `remappings.txt`, in addition to the ones described above (replace `0.3.6` with the version of the plugin that you installed):
Then add the following additional line to `remappings.txt`, in addition to the ones described above (replace `0.3.6` with the version of the plugin that you installed):
```
openzeppelin-foundry-upgrades/=dependencies/openzeppelin-foundry-upgrades-0.3.6/src/
solidity-stringutils/=dependencies/openzeppelin-foundry-upgrades-0.3.6/lib/solidity-stringutils/
```

## OpenZeppelin Defender integration
Expand All @@ -76,7 +74,7 @@ See [DEFENDER.md](DEFENDER.md)

## Foundry Requirements

This library requires [forge-std](https://github.com/foundry-rs/forge-std) version 1.8.0 or higher.
This library requires [forge-std](https://github.com/foundry-rs/forge-std) version 1.9.5 or higher.

## Before Running

Expand Down
6 changes: 2 additions & 4 deletions docs/modules/pages/foundry-upgrades.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,20 @@ Follow the steps above, but instead of running `forge install OpenZeppelin/openz
npm install @openzeppelin/foundry-upgrades
----

Then add the following additional lines to `remappings.txt`, in addition to the ones described above:
Then add the following additional line to `remappings.txt`, in addition to the ones described above:
[source,console]
----
openzeppelin-foundry-upgrades/=node_modules/@openzeppelin/foundry-upgrades/src/
solidity-stringutils/=node_modules/@openzeppelin/foundry-upgrades/lib/solidity-stringutils/
----

==== Soldeer

Follow the steps above, but instead of running `forge install OpenZeppelin/openzeppelin-foundry-upgrades`, use one of the install commands described in https://soldeer.xyz/project/openzeppelin-foundry-upgrades

Then add the following additional lines to `remappings.txt`, in addition to the ones described above (replace `0.3.6` with the version of the plugin that you installed):
Then add the following additional line to `remappings.txt`, in addition to the ones described above (replace `0.3.6` with the version of the plugin that you installed):
[source,console]
----
openzeppelin-foundry-upgrades/=dependencies/openzeppelin-foundry-upgrades-0.3.6/src/
solidity-stringutils/=dependencies/openzeppelin-foundry-upgrades-0.3.6/lib/solidity-stringutils/
----

== Foundry Requirements
Expand Down
1 change: 0 additions & 1 deletion lib/solidity-stringutils
Submodule solidity-stringutils deleted from 4b2fcc
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
"description": "Foundry library for deploying and managing upgradeable contracts",
"license": "MIT",
"files": [
"src/**/*",
"lib/solidity-stringutils/**/*"
"src/**/*"
],
"repository": {
"type": "git",
Expand Down
17 changes: 10 additions & 7 deletions src/internal/Core.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {console} from "forge-std/console.sol";
import {strings} from "solidity-stringutils/src/strings.sol";

import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

import {Options} from "../Options.sol";
import {Versions} from "./Versions.sol";
Expand Down Expand Up @@ -61,6 +62,8 @@ library Core {
upgradeProxy(proxy, contractName, data, opts);
}

using Strings for *;

/**
* @dev Upgrades a proxy to a new implementation contract. Only supported for UUPS or transparent proxies.
*
Expand All @@ -74,15 +77,15 @@ library Core {
bytes32 adminSlot = vm.load(proxy, ADMIN_SLOT);
if (adminSlot == bytes32(0)) {
string memory upgradeInterfaceVersion = getUpgradeInterfaceVersion(proxy);
if (upgradeInterfaceVersion.toSlice().equals("5.0.0".toSlice()) || data.length > 0) {
if (upgradeInterfaceVersion.equal("5.0.0") || data.length > 0) {
IUpgradeableProxy(proxy).upgradeToAndCall(newImpl, data);
} else {
IUpgradeableProxy(proxy).upgradeTo(newImpl);
}
} else {
address admin = address(uint160(uint256(adminSlot)));
string memory upgradeInterfaceVersion = getUpgradeInterfaceVersion(admin);
if (upgradeInterfaceVersion.toSlice().equals("5.0.0".toSlice()) || data.length > 0) {
if (upgradeInterfaceVersion.equal("5.0.0") || data.length > 0) {
IProxyAdmin(admin).upgradeAndCall(proxy, newImpl, data);
} else {
IProxyAdmin(admin).upgrade(proxy, newImpl);
Expand Down Expand Up @@ -300,8 +303,6 @@ library Core {
*/
bytes32 private constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

using strings for *;

/**
* @dev Gets the upgrade interface version string from a proxy or admin contract using the `UPGRADE_INTERFACE_VERSION()` getter.
* If the contract does not have the getter or the return data does not look like a string, this function returns an empty string.
Expand Down Expand Up @@ -345,9 +346,10 @@ library Core {
string memory stdout = string(result.stdout);

// CLI validate command uses exit code to indicate if the validation passed or failed.
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
if (result.exitCode == 0) {
// As an extra precaution, we also check stdout for "SUCCESS" to ensure it actually ran.
if (stdout.toSlice().contains("SUCCESS".toSlice())) {
if (vm.contains(stdout, "SUCCESS")) {
if (result.stderr.length > 0) {
// Prints warnings from stderr
console.log(string(result.stderr));
Expand All @@ -357,7 +359,7 @@ library Core {
revert(string(abi.encodePacked("Failed to run upgrade safety validation: ", stdout)));
}
} else {
if (stdout.toSlice().contains("FAILED".toSlice())) {
if (vm.contains(stdout, "FAILED")) {
if (result.stderr.length > 0) {
// Prints warnings from stderr
console.log(string(result.stderr));
Expand Down Expand Up @@ -470,6 +472,7 @@ library Core {

function _deployFromBytecode(bytes memory bytecode) private returns (address) {
address addr;
/// @solidity memory-safe-assembly
assembly {
addr := create(0, add(bytecode, 32), mload(bytecode))
}
Expand Down
81 changes: 47 additions & 34 deletions src/internal/DefenderDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {console} from "forge-std/console.sol";
import {strings} from "solidity-stringutils/src/strings.sol";

import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

Expand All @@ -18,8 +17,6 @@ import {ProposeUpgradeResponse, ApprovalProcessResponse} from "../Defender.sol";
* WARNING: DO NOT USE DIRECTLY. Use Defender.sol instead.
*/
library DefenderDeploy {
using strings for *;

function deploy(
string memory contractName,
bytes memory constructorData,
Expand Down Expand Up @@ -54,7 +51,7 @@ library DefenderDeploy {
) internal view returns (string[] memory) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);

if (!(defenderOpts.licenseType).toSlice().empty()) {
if (bytes(defenderOpts.licenseType).length != 0) {
if (defenderOpts.skipVerifySourceCode) {
revert("The `licenseType` option cannot be used when the `skipVerifySourceCode` option is `true`");
} else if (defenderOpts.skipLicenseType) {
Expand Down Expand Up @@ -86,14 +83,14 @@ library DefenderDeploy {
if (defenderOpts.skipVerifySourceCode) {
inputBuilder[i++] = "--verifySourceCode";
inputBuilder[i++] = "false";
} else if (!(defenderOpts.licenseType).toSlice().empty()) {
} else if (bytes(defenderOpts.licenseType).length != 0) {
inputBuilder[i++] = "--licenseType";
inputBuilder[i++] = string(abi.encodePacked('"', defenderOpts.licenseType, '"'));
} else if (!defenderOpts.skipLicenseType && !(contractInfo.license).toSlice().empty()) {
} else if (!defenderOpts.skipLicenseType && bytes(contractInfo.license).length != 0) {
inputBuilder[i++] = "--licenseType";
inputBuilder[i++] = string(abi.encodePacked('"', _toLicenseType(contractInfo), '"'));
}
if (!(defenderOpts.relayerId).toSlice().empty()) {
if (bytes(defenderOpts.relayerId).length != 0) {
inputBuilder[i++] = "--relayerId";
inputBuilder[i++] = defenderOpts.relayerId;
}
Expand All @@ -117,7 +114,7 @@ library DefenderDeploy {
inputBuilder[i++] = "--maxPriorityFeePerGas";
inputBuilder[i++] = Strings.toString(defenderOpts.txOverrides.maxPriorityFeePerGas);
}
if (!(defenderOpts.metadata).toSlice().empty()) {
if (bytes(defenderOpts.metadata).length != 0) {
inputBuilder[i++] = "--metadata";
inputBuilder[i++] = string(abi.encodePacked('"', vm.replace(defenderOpts.metadata, '"', '\\"'), '"'));
}
Expand All @@ -133,35 +130,37 @@ library DefenderDeploy {
return inputs;
}

using Strings for string;

function _toLicenseType(ContractInfo memory contractInfo) private pure returns (string memory) {
strings.slice memory id = contractInfo.license.toSlice();
if (id.equals("UNLICENSED".toSlice())) {
string memory id = contractInfo.license;
if (id.equal("UNLICENSED")) {
return "None";
} else if (id.equals("Unlicense".toSlice())) {
} else if (id.equal("Unlicense")) {
return "Unlicense";
} else if (id.equals("MIT".toSlice())) {
} else if (id.equal("MIT")) {
return "MIT";
} else if (id.equals("GPL-2.0-only".toSlice()) || id.equals("GPL-2.0-or-later".toSlice())) {
} else if (id.equal("GPL-2.0-only") || id.equal("GPL-2.0-or-later")) {
return "GNU GPLv2";
} else if (id.equals("GPL-3.0-only".toSlice()) || id.equals("GPL-3.0-or-later".toSlice())) {
} else if (id.equal("GPL-3.0-only") || id.equal("GPL-3.0-or-later")) {
return "GNU GPLv3";
} else if (id.equals("LGPL-2.1-only".toSlice()) || id.equals("LGPL-2.1-or-later".toSlice())) {
} else if (id.equal("LGPL-2.1-only") || id.equal("LGPL-2.1-or-later")) {
return "GNU LGPLv2.1";
} else if (id.equals("LGPL-3.0-only".toSlice()) || id.equals("LGPL-3.0-or-later".toSlice())) {
} else if (id.equal("LGPL-3.0-only") || id.equal("LGPL-3.0-or-later")) {
return "GNU LGPLv3";
} else if (id.equals("BSD-2-Clause".toSlice())) {
} else if (id.equal("BSD-2-Clause")) {
return "BSD-2-Clause";
} else if (id.equals("BSD-3-Clause".toSlice())) {
} else if (id.equal("BSD-3-Clause")) {
return "BSD-3-Clause";
} else if (id.equals("MPL-2.0".toSlice())) {
} else if (id.equal("MPL-2.0")) {
return "MPL-2.0";
} else if (id.equals("OSL-3.0".toSlice())) {
} else if (id.equal("OSL-3.0")) {
return "OSL-3.0";
} else if (id.equals("Apache-2.0".toSlice())) {
} else if (id.equal("Apache-2.0")) {
return "Apache-2.0";
} else if (id.equals("AGPL-3.0-only".toSlice()) || id.equals("AGPL-3.0-or-later".toSlice())) {
} else if (id.equal("AGPL-3.0-only") || id.equal("AGPL-3.0-or-later")) {
return "GNU AGPLv3";
} else if (id.equals("BUSL-1.1".toSlice())) {
} else if (id.equal("BUSL-1.1")) {
return "BSL 1.1";
} else {
revert(
Expand Down Expand Up @@ -217,7 +216,7 @@ library DefenderDeploy {
return parseProposeUpgradeResponse(stdout);
}

function parseProposeUpgradeResponse(string memory stdout) internal pure returns (ProposeUpgradeResponse memory) {
function parseProposeUpgradeResponse(string memory stdout) internal returns (ProposeUpgradeResponse memory) {
ProposeUpgradeResponse memory response;
response.proposalId = _parseLine("Proposal ID: ", stdout, true);
response.url = _parseLine("Proposal URL: ", stdout, false);
Expand All @@ -228,15 +227,29 @@ library DefenderDeploy {
string memory expectedPrefix,
string memory stdout,
bool required
) private pure returns (string memory) {
strings.slice memory delim = expectedPrefix.toSlice();
if (stdout.toSlice().contains(delim)) {
strings.slice memory slice = stdout.toSlice().copy().find(delim).beyond(delim);
) private returns (string memory) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
if (vm.contains(stdout, expectedPrefix)) {
// Get the substring after the prefix
string[] memory segments = vm.split(stdout, expectedPrefix);
if (segments.length > 2) {
revert(
string(
abi.encodePacked(
"Found multiple occurrences of prefix '",
expectedPrefix,
"' in output: ",
stdout
)
)
);
}
string memory suffix = segments[1];
// Remove any following lines
if (slice.contains("\n".toSlice())) {
slice = slice.split("\n".toSlice());
if (vm.contains(suffix, "\n")) {
suffix = vm.split(suffix, "\n")[0];
}
return slice.toString();
return suffix;
} else if (required) {
revert(
string(abi.encodePacked("Failed to find line with prefix '", expectedPrefix, "' in output: ", stdout))
Expand Down Expand Up @@ -276,7 +289,7 @@ library DefenderDeploy {
inputBuilder[i++] = "--proxyAdminAddress";
inputBuilder[i++] = vm.toString(proxyAdminAddress);
}
if (!(opts.defender.upgradeApprovalProcessId).toSlice().empty()) {
if (bytes(opts.defender.upgradeApprovalProcessId).length != 0) {
inputBuilder[i++] = "--approvalProcessId";
inputBuilder[i++] = opts.defender.upgradeApprovalProcessId;
}
Expand All @@ -303,15 +316,15 @@ library DefenderDeploy {
return parseApprovalProcessResponse(stdout);
}

function parseApprovalProcessResponse(string memory stdout) internal pure returns (ApprovalProcessResponse memory) {
function parseApprovalProcessResponse(string memory stdout) internal returns (ApprovalProcessResponse memory) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);

ApprovalProcessResponse memory response;

response.approvalProcessId = _parseLine("Approval process ID: ", stdout, true);

string memory viaString = _parseLine("Via: ", stdout, false);
if (viaString.toSlice().len() != 0) {
if (bytes(viaString).length != 0) {
response.via = vm.parseAddress(viaString);
}

Expand Down
46 changes: 46 additions & 0 deletions src/internal/StringFinder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {Utils} from "./Utils.sol";

/**
* String finder functions using Forge's string cheatcodes.
* For internal use only.
*/
library StringFinder {
/**
* Returns whether the subject string contains the search string.
*/
function contains(string memory subject, string memory search) internal returns (bool) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
Copy link

@Amxx Amxx Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thing that can be defined only once as a library constant:

library StringFinder {
    Vm private constant VM = Vm(Utils.CHEATCODE_ADDRESS);
    
    function contains(string memory subject, string memory search) internal returns (bool) {
        return VM.contains(subject, search);
    }
    
    function startsWith(string memory subject, string memory search) internal pure returns (bool) {
        return VM.indexOf(subject, search) == 0;
    }
    
    // ...
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work on some older versions of Solidity 0.8.x which should be supported for previous versions of OpenZeppelin Contracts.

return vm.contains(subject, search);
}

/**
* Returns whether the subject string starts with the search string.
*/
function startsWith(string memory subject, string memory search) internal pure returns (bool) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
uint256 index = vm.indexOf(subject, search);
return index == 0;
Comment on lines +24 to +26
Copy link

@Amxx Amxx Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do without the Vm:

return return bytes(subject).length >= bytes(search).length
    && string(bytes(subject).slice(0, bytes(search).length)).equal(search);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that it would be pure, without would allow all the previously view function to remain view

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use Bytes.slice here because we want to support OpenZeppelin Contracts 4.x and 5.x, and Bytes isn't available in some older versions. We also can't just copy in the Bytes.slice code because older versions of Solidity don't support mcopy.

}

/**
* Returns whether the subject string ends with the search string.
*/
function endsWith(string memory subject, string memory search) internal pure returns (bool) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
string[] memory tokens = vm.split(subject, search);
return tokens.length > 1 && bytes(tokens[tokens.length - 1]).length == 0;
}

/**
* Returns the number of non-overlapping occurrences of the search string in the subject string.
*/
function count(string memory subject, string memory search) internal pure returns (uint256) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
string[] memory tokens = vm.split(subject, search);
return tokens.length - 1;
}
}
Loading
Loading