My last two entries in this series were both about SimpleStorage.sol — a contract that saves data by ID, and a fix for a missing access control check on it. The next step was different in kind, not just in complexity: instead of writing one contract, I needed to write one that deploys and tracks other contracts. A factory.
The problem: my data structure didn't match the pattern I was building on top of
I followed a tutorial for the base factory structure, but I've been customizing every tutorial in this series rather than copying it directly — SimpleStorage.sol uses a mapping and a struct, where the tutorial I originally learned from used an array. That decision paid off when it came to reading a single value out by ID, but it created a real problem once I needed to build a factory on top of it.
The tutorial's factory pattern assumed each deployed contract exposed its data as an array, so pulling a value back out meant array-style indexing. My deployed contracts expose their data as a mapping(uint256 => EntryData), keyed by an ID rather than a position. Reaching into a different, already-deployed contract's mapping — not just my own — and pulling a specific entry back out wasn't something the tutorial covered, because it wasn't built around that data structure.
How I actually worked through it
Before asking for help, I told the AI I was working with directly that I didn't want generated code — I wanted to be walked toward the answer, not handed it. What followed was closer to a Socratic dialogue than a code request: guiding questions rather than a solution, similar to how I used to walk cybersecurity students toward an answer during training rather than giving it to them outright. That process is what produced this function:
// reads from a deployed SimpleStorage contract
function SfGet(uint256 _SimpleStorageDataID, uint256 _ID) public view returns (string memory, address) {
return ListOfSimpleStorageContracts[_SimpleStorageDataID].DataIdToData(_ID);
}
Breaking down what this actually does
Two separate problems needed solving in this one function, and they're easy to conflate if you haven't hit this pattern before.
First: which contract? The factory can deploy many SimpleStorage instances, tracked in a list — ListOfSimpleStorageContracts — inside factory.sol. _SimpleStorageDataID is an index into that list, selecting which deployed contract instance to talk to. This is a different concept from the ID used inside SimpleStorage itself, even though both are uint256 — one picks a contract, the other picks an entry inside it.
Second: which entry, inside that contract? Once you've selected a deployed instance, _ID is the key into that instance's mapping — the same DataIdToData mapping from the earlier entries in this series. .DataIdToData(_ID) calls the auto-generated getter Solidity creates for a public mapping, but it's being called on a specific deployed contract reference (ListOfSimpleStorageContracts[_SimpleStorageDataID]), not on this contract.
So the function needs two IDs because there are two levels of lookup happening — which contract, then which entry in it — and conflating them is exactly the kind of bug that would silently return the wrong data instead of failing loudly.
Why the return type includes an address
SimpleStorage's ownership check — the access control fix from the last entry in this series — depends entirely on Owner, an address stored per entry. Reading data back out through the factory has to preserve that: the function returns (string memory, address), not just the string. Any caller or contract using this read function to make a decision — say, deciding whether to allow an action based on ownership — needs that address returned accurately alongside the data. Dropping it, or getting the ordering wrong, would quietly break the same access control logic the earlier fix depended on, just one layer further up the call chain.
Check the full factory.sol here
Where this is headed
Factories like this are a common building block once you get past single, standalone contracts — most DeFi protocols run on some version of "one contract deploys and tracks many instances of another." That's the direction I'm heading next: more DeFi-style projects, building on this pattern rather than starting from scratch each time.
Top comments (0)