diff --git a/tests/bounties/Makefile b/tests/bounties/Makefile index 2f2912d..34f5ee7 100644 --- a/tests/bounties/Makefile +++ b/tests/bounties/Makefile @@ -96,3 +96,18 @@ dist/adder-%-bounty.tar.xz: \ build/adder-%-bounty/src/Adder.sol: src/adder/src/%/Adder.sol mkdir -p $(@D) cp $< $@ + +################### +# OpenZeppelin +################### + +all: dist/openzeppelin-bounty.tar.xz + +dist/openzeppelin-bounty.tar.xz: \ + src/openzeppelin/setup-exec-env.sh \ + src/openzeppelin/start.sh \ + src/openzeppelin/foundry.toml \ + src/openzeppelin/src/IRegistry.sol \ + src/openzeppelin/src/Registry.sol \ + src/openzeppelin/src/Counter.sol + tar $(TAR_OPTS) $@ $^ \ No newline at end of file diff --git a/tests/bounties/src/openzeppelin/foundry.toml b/tests/bounties/src/openzeppelin/foundry.toml new file mode 100644 index 0000000..9485f39 --- /dev/null +++ b/tests/bounties/src/openzeppelin/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +remappings = [ + "@openzeppelin/contracts/=/usr/share/forge-lib/openzeppelin-contracts/contracts", +] diff --git a/tests/bounties/src/openzeppelin/setup-exec-env.sh b/tests/bounties/src/openzeppelin/setup-exec-env.sh new file mode 100755 index 0000000..a1a1cc2 --- /dev/null +++ b/tests/bounties/src/openzeppelin/setup-exec-env.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail +shopt -s expand_aliases + +SOLC_VERSION=0.8.28 + +FOUNDRY_REF=2cdbfac +alias cast="cast-$FOUNDRY_REF" +alias forge="forge-$FOUNDRY_REF" + +RETH_VERSION=1.0.5 +alias reth="reth-$RETH_VERSION" + +>&2 echo "Setting up Forge project..." +cp -r src /tmp +cp foundry.toml /tmp +cp "$1" /tmp/src/Exploit.sol +cd /tmp + +>&2 echo "Building Forge project..." +forge build --use $(which solc-$SOLC_VERSION) + +HTTP_ADDR=127.0.0.1 +HTTP_PORT=8545 + +>&2 echo "Starting up Reth..." +reth node \ + --dev \ + --quiet \ + --http.addr $HTTP_ADDR \ + --http.port $HTTP_PORT \ + --log.file.max-files 0 \ + --datadir .local/share/reth & + +reth_pid=$! +trap 'kill $reth_pid' EXIT + +export ETH_RPC_URL=$HTTP_ADDR:$HTTP_PORT + +while true +do + if chain_id=$(cast chain-id 2>/dev/null) + then + if [[ $chain_id == 1337 ]] + then + >&2 echo "Reth is listening." + break + else + >&2 echo "Reth has unexpected chain ID $chain_id." + exit 1 + fi + else + if kill -0 $reth_pid + then + >&2 echo "Waiting for Reth to start listening..." + sleep 1 + else + >&2 echo "Reth exited..." + exit 1 + fi + fi +done + +PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + +deploy() { + forge create --json --private-key "$PK" "$1:$2" | jq -r .deployedTo +} + +>&2 echo "Deploying registry contract..." +REGISTRY=$(deploy src/Registry.sol Registry) + +send() { + >/dev/null cast send --private-key "$PK" "$@" +} + +deploy_and_register() { + address=$(deploy "$@") + send "$REGISTRY" 'set(string,address)' "$2" "$address" + echo "$address" +} diff --git a/tests/bounties/src/openzeppelin/src/Counter.sol b/tests/bounties/src/openzeppelin/src/Counter.sol new file mode 100644 index 0000000..94c5c31 --- /dev/null +++ b/tests/bounties/src/openzeppelin/src/Counter.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract Counter is Ownable { + uint256 public number; + + constructor() Ownable(msg.sender) {} + + function setNumber(uint256 newNumber) public onlyOwner { + number = newNumber; + } + + function increment() public onlyOwner { + number++; + } +} diff --git a/tests/bounties/src/openzeppelin/src/IRegistry.sol b/tests/bounties/src/openzeppelin/src/IRegistry.sol new file mode 100644 index 0000000..61dde95 --- /dev/null +++ b/tests/bounties/src/openzeppelin/src/IRegistry.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.28; + +interface IRegistry { + function get(string memory name) external view returns (address addr); + function set(string memory name, address addr) external; +} diff --git a/tests/bounties/src/openzeppelin/src/Registry.sol b/tests/bounties/src/openzeppelin/src/Registry.sol new file mode 100644 index 0000000..596b942 --- /dev/null +++ b/tests/bounties/src/openzeppelin/src/Registry.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.28; + +import {IRegistry} from "./IRegistry.sol"; + +contract Registry is IRegistry { + mapping(string name => address addr) public get; + + function set(string memory name, address addr) external override { + get[name] = addr; + } +} diff --git a/tests/bounties/src/openzeppelin/start.sh b/tests/bounties/src/openzeppelin/start.sh new file mode 100755 index 0000000..1e1a35f --- /dev/null +++ b/tests/bounties/src/openzeppelin/start.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +source ./setup-exec-env.sh + +>&2 echo "Deploying and registering project contracts..." +COUNTER=$(deploy_and_register src/Counter.sol Counter) + +>&2 echo "Deploying exploit contract..." +EXPLOIT=$(deploy src/Exploit.sol Exploit) + +>&2 echo "Running exploit..." +send "$EXPLOIT" 'run(address)' "$REGISTRY" + +>&2 echo "Verifying contracts after exploit execution..." +number=$(cast call "$COUNTER" 'increment()(uint256)') + +if [ "$number" -eq 0 ] +then + >&2 echo "No exploit found." + exit 1 +else + >&2 echo "Valid exploit!" + exit 0 +fi diff --git a/tests/tests.lua b/tests/tests.lua index d16847d..f28d800 100644 --- a/tests/tests.lua +++ b/tests/tests.lua @@ -944,5 +944,28 @@ describe("tests on Adder bounty", function() end) end) +describe("tests on OpenZeppelin bounty", function() + local name = "openzeppelin" + local description = "Try to break the Counter smart contract written in Solidity and dependent of OpenZeppelin" + local bounty_code = "tests/bounties/dist/openzeppelin-bounty.tar.xz" + local bounty_deadline = timestamp + 3600 + + it("should create bounty", function() + local res = advance_input(machine, { + sender = DEVELOPER1_WALLET, + kind = "CreateAppBounty", + timestamp = timestamp, + data = { + name = name, + description = description, + deadline = bounty_deadline, + token = CTSI_ADDRESS, + codeZipBinary = tobase64(readfile(bounty_code)), + }, + }) + expect.equal(res.status, "accepted") + end) +end) + lester.report() -- Print overall statistic of the tests run. lester.exit() -- Exit with success if all tests passed.