Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 85 additions & 32 deletions contracts/Dispenser.sol
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ error WrongAmount(uint256 provided, uint256 expected);
/// @dev Caught reentrancy violation.
error ReentrancyGuard();

/// @dev The contract is already initialized.
error AlreadyInitialized();

/// @dev The contract is paused.
error Paused();

Expand Down Expand Up @@ -263,7 +266,7 @@ contract Dispenser {
}

event OwnerUpdated(address indexed owner);
event TokenomicsUpdated(address indexed tokenomics);
event ImplementationUpdated(address indexed implementation);
event TreasuryUpdated(address indexed treasury);
event VoteWeightingUpdated(address indexed voteWeighting);
event StakingParamsUpdated(uint256 maxNumClaimingEpochs, uint256 maxNumStakingTargets);
Expand All @@ -280,17 +283,29 @@ contract Dispenser {
event AddNomineeHash(bytes32 indexed nomineeHash);
event RemoveNomineeHash(bytes32 indexed nomineeHash);

// Dispenser proxy address slot
// keccak256("PROXY_DISPENSER") = "0x8bd249c73459f2c50400ebdc57436101fc7d9a76908baf1ba5be362b47b48f83"
bytes32 public constant PROXY_DISPENSER = 0x8bd249c73459f2c50400ebdc57436101fc7d9a76908baf1ba5be362b47b48f83;
// Maximum chain Id as per EVM specs
uint256 public constant MAX_EVM_CHAIN_ID = type(uint64).max / 2 - 36;

// Immutables live in the implementation bytecode, not proxy storage: they are re-settable by deploying
// a new implementation and calling changeImplementation. Every new implementation deploy must pass the
// correct constructor args — a wrong value silently rewires the proxy.
uint256 public immutable defaultMinStakingWeight;
uint256 public immutable defaultMaxStakingIncentive;
// OLAS token address
address public immutable olas;
// Tokenomics proxy address (stable by construction — the proxy address never changes)
address public immutable tokenomics;
// Retainer address in bytes32 form
bytes32 public immutable retainer;
// Retainer hash of a Nominee struct composed of retainer address with block.chainid
bytes32 public immutable retainerHash;

// Storage layout below is consumed via the DispenserProxy delegatecall and is FROZEN: never remove,
// reorder, retype or insert in the middle — new variables are appended strictly after the last mapping.

// Max number of epochs to claim staking incentives for
uint256 public maxNumClaimingEpochs;
// Max number of targets for a specific chain to claim staking incentives for
Expand All @@ -302,8 +317,6 @@ contract Dispenser {
// Pause state
Pause public paused;

// Tokenomics contract address
address public tokenomics;
// Treasury contract address
address public treasury;
// Vote Weighting contract address
Expand All @@ -320,36 +333,27 @@ contract Dispenser {
// Mapping for epoch number => refunded all the staking inflation due to zero total voting power
mapping(uint256 => bool) public mapZeroWeightEpochRefunded;

/// @dev Dispenser constructor.
/// @dev Dispenser implementation constructor: sets the implementation-bytecode immutables only.
/// @notice The mutable state is set via initialize() delegatecall-ed by the DispenserProxy constructor.
/// @param _olas OLAS token address.
/// @param _tokenomics Tokenomics address.
/// @param _treasury Treasury address.
/// @param _voteWeighting Vote Weighting address.
/// @param _tokenomics Tokenomics proxy address.
/// @param _retainer Retainer address in bytes32 form.
/// @param _defaultMinStakingWeight Default min staking weight (bounded by uint16).
/// @param _defaultMaxStakingIncentive Default max staking incentive (bounded by uint96).
constructor(
address _olas,
address _tokenomics,
address _treasury,
address _voteWeighting,
bytes32 _retainer,
uint256 _maxNumClaimingEpochs,
uint256 _maxNumStakingTargets,
uint256 _defaultMinStakingWeight,
uint256 _defaultMaxStakingIncentive
) {
owner = msg.sender;
_locked = 1;
// Staking incentives must be paused at the time of deployment because staking parameters are not live yet
paused = Pause.StakingIncentivesPaused;

// Check for at least one zero contract address
if (_olas == address(0) || _tokenomics == address(0) || _treasury == address(0) ||
_voteWeighting == address(0) || _retainer == 0) {
if (_olas == address(0) || _tokenomics == address(0) || _retainer == 0) {
revert ZeroAddress();
}

// Check for zero value staking parameters
if (_maxNumClaimingEpochs == 0 || _maxNumStakingTargets == 0 || _defaultMinStakingWeight == 0 ||
_defaultMaxStakingIncentive == 0) {
if (_defaultMinStakingWeight == 0 || _defaultMaxStakingIncentive == 0) {
revert ZeroValue();
}

Expand All @@ -363,17 +367,71 @@ contract Dispenser {

olas = _olas;
tokenomics = _tokenomics;
treasury = _treasury;
voteWeighting = _voteWeighting;

retainer = _retainer;
retainerHash = keccak256(abi.encode(IVoteWeighting.Nominee(retainer, block.chainid)));
maxNumClaimingEpochs = _maxNumClaimingEpochs;
maxNumStakingTargets = _maxNumStakingTargets;
defaultMinStakingWeight = _defaultMinStakingWeight;
defaultMaxStakingIncentive = _defaultMaxStakingIncentive;
}

/// @dev Initializes the dispenser proxy storage.
/// @notice Delegatecall-ed by the DispenserProxy constructor; the caller becomes the owner.
/// @param _treasury Treasury address.
/// @param _voteWeighting Vote Weighting address.
/// @param _maxNumClaimingEpochs Maximum number of epochs to claim staking incentives for.
/// @param _maxNumStakingTargets Maximum number of staking targets on a single chain Id.
function initialize(
address _treasury,
address _voteWeighting,
uint256 _maxNumClaimingEpochs,
uint256 _maxNumStakingTargets
) external {
// Check that the proxy storage has not been initialized yet
if (owner != address(0)) {
revert AlreadyInitialized();
}

// Check for at least one zero contract address
if (_treasury == address(0) || _voteWeighting == address(0)) {
revert ZeroAddress();
}

// Check for zero value staking parameters
if (_maxNumClaimingEpochs == 0 || _maxNumStakingTargets == 0) {
revert ZeroValue();
}

owner = msg.sender;
_locked = 1;
// Staking incentives must be paused at the time of deployment because staking parameters are not live yet
paused = Pause.StakingIncentivesPaused;

treasury = _treasury;
voteWeighting = _voteWeighting;
maxNumClaimingEpochs = _maxNumClaimingEpochs;
maxNumStakingTargets = _maxNumStakingTargets;
}

/// @dev Changes the dispenser implementation contract address behind the proxy.
/// @param implementation Dispenser implementation contract address.
function changeImplementation(address implementation) external {
// Check for the contract ownership
if (msg.sender != owner) {
revert OwnerOnly(msg.sender, owner);
}

// Check for the zero address
if (implementation == address(0)) {
revert ZeroAddress();
}

// Store the implementation address under the designated storage slot
assembly {
sstore(PROXY_DISPENSER, implementation)
}
emit ImplementationUpdated(implementation);
}

/// @dev Checkpoints specified staking target (nominee in Vote Weighting) and gets claimed epoch counters.
/// @param nomineeHash Hash of a Nominee(stakingTarget, chainId).
/// @param numClaimedEpochs Specified number of claimed epochs.
Expand Down Expand Up @@ -685,21 +743,16 @@ contract Dispenser {
}

/// @dev Changes various managing contract addresses.
/// @param _tokenomics Tokenomics address.
/// @notice The tokenomics address is an implementation immutable (it points at the stable TokenomicsProxy);
/// changing it requires deploying a new implementation and calling changeImplementation.
/// @param _treasury Treasury address.
/// @param _voteWeighting Vote Weighting address.
function changeManagers(address _tokenomics, address _treasury, address _voteWeighting) external {
function changeManagers(address _treasury, address _voteWeighting) external {
// Check for the contract ownership
if (msg.sender != owner) {
revert OwnerOnly(msg.sender, owner);
}

// Change Tokenomics contract address
if (_tokenomics != address(0)) {
tokenomics = _tokenomics;
emit TokenomicsUpdated(_tokenomics);
}

// Change Treasury contract address
if (_treasury != address(0)) {
treasury = _treasury;
Expand Down
68 changes: 68 additions & 0 deletions contracts/proxies/DispenserProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

/// @dev Proxy initialization failed.
error InitializationFailed();

/// @dev Zero address.
error ZeroAddress();

/// @dev Zero Value.
error ZeroValue();

/*
* This is a proxy contract for dispenser.
* Proxy implementation is created based on the Universal Upgradeable Proxy Standard (UUPS) EIP-1822.
* The implementation address must be located in a unique storage slot of the proxy contract.
* The upgrade logic must be located in the implementation contract.
* Special dispenser implementation address slot is produced by hashing the "PROXY_DISPENSER" string
* in order to make the slot unique.
* The fallback() implementation for all the delegatecall-s is inspired by the Gnosis Safe set of contracts.
*/

/// @title DispenserProxy - Smart contract for dispenser proxy
/// @author Aleksandr Kuperman - <aleksandr.kuperman@valory.xyz>
/// @author Andrey Lebedev - <andrey.lebedev@valory.xyz>
contract DispenserProxy {
// Code position in storage is keccak256("PROXY_DISPENSER") = "0x8bd249c73459f2c50400ebdc57436101fc7d9a76908baf1ba5be362b47b48f83"
bytes32 public constant PROXY_DISPENSER = 0x8bd249c73459f2c50400ebdc57436101fc7d9a76908baf1ba5be362b47b48f83;

/// @dev DispenserProxy constructor.
/// @param dispenser Dispenser implementation address.
/// @param dispenserData Dispenser initialization data.
constructor(address dispenser, bytes memory dispenserData) {
// Check for the zero address, since the delegatecall works even with the zero one
if (dispenser == address(0)) {
revert ZeroAddress();
}

// Check for the zero data
if (dispenserData.length == 0) {
revert ZeroValue();
}

assembly {
sstore(PROXY_DISPENSER, dispenser)
}
// Initialize proxy dispenser storage
(bool success, ) = dispenser.delegatecall(dispenserData);
if (!success) {
revert InitializationFailed();
}
}

/// @dev Delegatecall to all the incoming data.
fallback() external payable {
assembly {
let dispenser := sload(PROXY_DISPENSER)
// Otherwise continue with the delegatecall to the dispenser implementation
calldatacopy(0, 0, calldatasize())
let success := delegatecall(gas(), dispenser, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
if eq(success, 0) {
revert(0, returndatasize())
}
return(0, returndatasize())
}
}
}
27 changes: 18 additions & 9 deletions test/Dispenser.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.30;
import {Test} from "forge-std/Test.sol";
import {Utils} from "./utils/Utils.sol";
import {Dispenser} from "../contracts/Dispenser.sol";
import {DispenserProxy} from "../contracts/proxies/DispenserProxy.sol";
import "../contracts/Tokenomics.sol";
import {TokenomicsProxy} from "../contracts/proxies/TokenomicsProxy.sol";
import {Treasury} from "../contracts/Treasury.sol";
Expand Down Expand Up @@ -56,24 +57,32 @@ contract BaseSetup is Test {
componentRegistry = new MockRegistry();
agentRegistry = new MockRegistry();
serviceRegistry = new MockRegistry();
dispenser = new Dispenser(address(olas), deployer, deployer, deployer, retainer, 100, 100, 100, 1 ether);

// Depository contract is irrelevant here, so we are using a deployer's address
// Correct tokenomics address will be added below
treasury = new Treasury(address(olas), deployer, deployer, address(dispenser));
// Depository and dispenser contracts are irrelevant at this point, so we are using a deployer's address
// Correct tokenomics and dispenser addresses will be added below
treasury = new Treasury(address(olas), deployer, deployer, deployer);

Tokenomics tokenomicsMaster = new Tokenomics();
// Depository contract is irrelevant here, so we are using a deployer's address
// Dispenser address is a placeholder until the dispenser proxy is deployed below
bytes memory proxyData = abi.encodeWithSelector(tokenomicsMaster.initializeTokenomics.selector,
address(olas), address(treasury), deployer, address(dispenser), address(ve), epochLen,
address(olas), address(treasury), deployer, deployer, address(ve), epochLen,
address(componentRegistry), address(agentRegistry), address(serviceRegistry), address(0));
TokenomicsProxy tokenomicsProxy = new TokenomicsProxy(address(tokenomicsMaster), proxyData);
tokenomics = Tokenomics(address(tokenomicsProxy));

// Change tokenomics address
treasury.changeManagers(address(tokenomics), address(0), address(0));
// Change the tokenomics and treasury addresses in the dispenser to correct ones
dispenser.changeManagers(address(tokenomics), address(treasury), address(0));
// Deploy dispenser implementation (tokenomics proxy address is an implementation immutable) and proxy
// Vote Weighting contract is irrelevant here, so we are using a deployer's address
Dispenser dispenserMaster = new Dispenser(address(olas), address(tokenomics), retainer, 100, 1 ether);
bytes memory dispenserData = abi.encodeWithSelector(dispenserMaster.initialize.selector,
address(treasury), deployer, 100, 100);
DispenserProxy dispenserProxy = new DispenserProxy(address(dispenserMaster), dispenserData);
dispenser = Dispenser(address(dispenserProxy));

// Change tokenomics and dispenser addresses in treasury
treasury.changeManagers(address(tokenomics), address(0), address(dispenser));
// Change the dispenser address in tokenomics to the correct one
tokenomics.changeManagers(address(0), address(0), address(dispenser));

// Set treasury contract as a minter for OLAS
olas.changeMinter(address(treasury));
Expand Down
Loading
Loading