Solidity Proxy Concepts
A proxy is a contract that holds state and forwards calls to a separate logic contract. The logic contract holds the code, while the proxy holds the values, so one logic contract can serve many proxies.
A value set through the proxy reads back through the proxy
The logic contract sets a value. The call goes through the proxy, so the proxy stores the value.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract Logic {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract Proxy {
bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
constructor(address impl) {
_setImplementation(impl);
}
function implementation() public view returns (address impl) {
bytes32 slot = IMPL_SLOT;
assembly { impl := sload(slot) }
}
function _setImplementation(address impl) internal {
bytes32 slot = IMPL_SLOT;
assembly { sstore(slot, impl) }
}
fallback() external payable {
address impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}The logic contract keeps its own value untouched
Setting a value through the proxy changes proxy storage only. The logic contract still reads its own starting value.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract Logic {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract Proxy {
bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
constructor(address impl) {
_setImplementation(impl);
}
function implementation() public view returns (address impl) {
bytes32 slot = IMPL_SLOT;
assembly { impl := sload(slot) }
}
function _setImplementation(address impl) internal {
bytes32 slot = IMPL_SLOT;
assembly { sstore(slot, impl) }
}
fallback() external payable {
address impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}Two proxies share one logic contract with separate values
Both proxies point at the same logic contract. Each proxy still stores its own value.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract Logic {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract Proxy {
bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
constructor(address impl) {
_setImplementation(impl);
}
function implementation() public view returns (address impl) {
bytes32 slot = IMPL_SLOT;
assembly { impl := sload(slot) }
}
function _setImplementation(address impl) internal {
bytes32 slot = IMPL_SLOT;
assembly { sstore(slot, impl) }
}
fallback() external payable {
address impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract Counter {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
function increment() public {
value += 1;
}
}
contract Proxy {
bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
constructor(address impl) {
_setImplementation(impl);
}
function implementation() public view returns (address impl) {
bytes32 slot = IMPL_SLOT;
assembly { impl := sload(slot) }
}
function _setImplementation(address impl) internal {
bytes32 slot = IMPL_SLOT;
assembly { sstore(slot, impl) }
}
fallback() external payable {
address impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}Key points
- Proxy: a contract that stores state and forwards calls to a logic contract.
- Logic contract: a contract that holds the code the proxy runs.
- Delegatecall: the forwarding call that runs logic code in the proxy storage.
- Shared logic: many proxies can point at one logic contract and keep separate values.
- State stays put: a value set through the proxy lives in the proxy, not in the logic contract.
In this example, a counter logic contract sets and raises a value, and a proxy forwards both calls. The proxy ends at 7 while the logic contract itself stays at 0, which shows the state lives in the proxy.
Note
the proxy keeps its logic address in a separate slot, so it never collides with logic values.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
Where does a value set through a proxy live?