Implementation Contract

The logic contract containing actual business functions that executes in proxy storage context via delegatecall.

An implementation contract contains the actual business logic and functions that get executed when users interact with a proxy contract. Unlike traditional contracts that manage their own storage, implementation contracts execute in the context of the proxy's storage through Ethereum's delegatecall mechanism.

How Implementation Contracts Work

The implementation contract is deployed separately from the proxy and contains all the business logic:

1contract TokenImplementation {
2 // Storage variables (layout must match proxy)
3 mapping(address => uint256) private _balances;
4 mapping(address => mapping(address => uint256)) private _allowances;
5 uint256 private _totalSupply;
6 string private _name;
7 string private _symbol;
8
9 // Business logic functions
10 function transfer(address to, uint256 amount) external returns (bool) {
11 address owner = msg.sender;
12 _transfer(owner, to, amount);
13 return true;
14 }
15
16 function _transfer(address from, address to, uint256 amount) internal {
17 require(from != address(0), "Transfer from zero address");
18 require(to != address(0), "Transfer to zero address");
19
20 uint256 fromBalance = _balances[from];
21 require(fromBalance >= amount, "Transfer exceeds balance");
22
23 unchecked {
24 _balances[from] = fromBalance - amount;
25 _balances[to] += amount;
26 }
27
28 emit Transfer(from, to, amount);
29 }
30}

When the proxy receives a call to transfer(), it forwards the call to the implementation contract using delegatecall. The implementation's code executes, but all storage operations happen in the proxy's storage space.

Key Characteristics

Stateless Execution

Implementation contracts should not rely on their own storage—all state lives in the proxy:

1// WRONG: Implementation trying to use its own storage
2contract BadImplementation {
3 uint256 public version = 1; // This storage won't persist!
4
5 function getVersion() external view returns (uint256) {
6 return version; // Will return 0 when called via proxy
7 }
8}
9
10// CORRECT: All state managed through proxy storage
11contract GoodImplementation {
12 uint256 private _version;
13
14 function initialize(uint256 version) external {
15 _version = version; // Stored in proxy's storage
16 }
17}

Constructor Limitations

Constructors don't execute in proxy context since they run during deployment, not during delegatecall:

1// WRONG: Constructor won't affect proxy state
2contract BadImplementation {
3 address public owner;
4
5 constructor(address _owner) {
6 owner = _owner; // Only sets implementation's storage, not proxy's!
7 }
8}
9
10// CORRECT: Use initializer function
11contract GoodImplementation {
12 address public owner;
13
14 function initialize(address _owner) external {
15 require(owner == address(0), "Already initialized");
16 owner = _owner; // Sets proxy's storage
17 }
18}

Security Considerations

Storage Layout Compatibility

The implementation's storage layout must be compatible with the proxy and all future versions:

1// Version 1
2contract ImplementationV1 {
3 address public owner; // Slot 0
4 uint256 public totalSupply; // Slot 1
5}
6
7// Version 2 - SAFE upgrade (append-only)
8contract ImplementationV2 {
9 address public owner; // Slot 0 (unchanged)
10 uint256 public totalSupply; // Slot 1 (unchanged)
11 bool public paused; // Slot 2 (new)
12}
13
14// Version 2 - UNSAFE upgrade (layout changes)
15contract UnsafeImplementationV2 {
16 bool public paused; // Slot 0 - COLLISION with owner!
17 address public owner; // Slot 1 - COLLISION with totalSupply!
18 uint256 public totalSupply; // Slot 2 - Data corruption!
19}

Unprotected Functions

Implementation contracts may contain dangerous functions if called directly:

1contract VulnerableImplementation {
2 address public owner;
3
4 // DANGEROUS: No protection against direct calls
5 function initialize(address _owner) external {
6 owner = _owner; // Can be called on implementation directly!
7 }
8
9 function emergencyWithdraw() external {
10 require(msg.sender == owner, "Not owner");
11 // If called on implementation directly, uses implementation's owner!
12 payable(owner).transfer(address(this).balance);
13 }
14}

Self-Destruct Vulnerability

If an implementation contains selfdestruct, it can brick all proxies using it:

1contract DangerousImplementation {
2 function destroy() external onlyOwner {
3 selfdestruct(payable(owner)); // Destroys implementation for ALL proxies!
4 }
5}

Best Practices

1. Use Initializers, Not Constructors

Always use initializer functions with replay protection:

1import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
2
3contract SecureImplementation is Initializable {
4 function initialize(address _owner) external initializer {
5 // Safe initialization that runs once per proxy
6 }
7}

2. Disable Initializers in Implementation

Prevent direct initialization of the implementation contract:

1contract SecureImplementation is Initializable {
2 constructor() {
3 _disableInitializers(); // Prevents initialization of implementation
4 }
5}

3. Storage Gaps for Future Upgrades

Reserve storage slots for future variables:

1contract BaseImplementation {
2 address public owner;
3
4 // Reserve 50 slots for future upgrades
5 uint256[50] private __gap;
6}

4. Separate Implementation and Proxy Logic

Never mix proxy management with business logic:

1// WRONG: Business logic mixed with proxy logic
2contract BadImplementation {
3 function upgrade(address newImpl) external onlyOwner {
4 // Proxy upgrade logic mixed with business logic - dangerous!
5 }
6
7 function transfer(address to, uint256 amount) external {
8 // Business logic
9 }
10}
11
12// CORRECT: Pure business logic only
13contract GoodImplementation {
14 function transfer(address to, uint256 amount) external {
15 // Pure business logic only
16 }
17 // Proxy management handled separately
18}

Testing Implementation Contracts

Always test both direct calls and proxy calls:

1describe("Implementation Contract", function() {
2 it("should work when called through proxy", async function() {
3 const proxy = await deployProxy(Implementation, [owner.address]);
4 await expect(proxy.transfer(user.address, 100)).to.emit(proxy, "Transfer");
5 });
6
7 it("should be unusable when called directly", async function() {
8 const impl = await Implementation.deploy();
9 // Direct calls to implementation should fail or be meaningless
10 await expect(impl.transfer(user.address, 100)).to.be.reverted;
11 });
12});

Implementation contracts are the brain of upgradeable systems, but they require careful design to ensure they work correctly in the proxy context while maintaining security across upgrades.

Need expert guidance on Implementation Contract?

Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.

Get a Quote

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx