Skip to content
Merged
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
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,7 @@ deploy-governorv2-implementation-base-mainnet :; forge script script/SeamGoverno
deploy-governorv2-implementation-tenderly :; forge script script/SeamGovernorUpgradeV2.s.sol:SeamGovernorUpgradeV2 --force --rpc-url tenderly --slow --broadcast -vvvv --verify --verifier-url ${TENDERLY_FORK_VERIFIER_URL} --etherscan-api-key ${TENDERLY_ACCESS_KEY}

deploy-staked-token-implementation-base-mainnet :; forge script script/StakedTokenImplementation.s.sol:StakedTokenImplementation --force --rpc-url base --slow --broadcast --verify --delay 5 --verifier-url ${VERIFIER_URL} -vvvv
deploy-staked-token-implementation-tenderly :; forge script script/StakedTokenImplementation.s.sol:StakedTokenImplementation --force --rpc-url tenderly --slow --broadcast -vvvv --verify --verifier-url ${TENDERLY_FORK_VERIFIER_URL} --etherscan-api-key ${TENDERLY_ACCESS_KEY}
deploy-staked-token-implementation-tenderly :; forge script script/StakedTokenImplementation.s.sol:StakedTokenImplementation --force --rpc-url tenderly --slow --broadcast -vvvv --verify --verifier-url ${TENDERLY_FORK_VERIFIER_URL} --etherscan-api-key ${TENDERLY_ACCESS_KEY}

deploy-emission-manager-v2-base-mainnet :; forge script script/SeamEmissionManagerV2Deploy.s.sol:SeamEmissionManagerV2Deploy --force --rpc-url base --slow --broadcast --verify --delay 5 --verifier-url ${VERIFIER_URL} -vvvv
deploy-emission-manager-v2-tenderly :; forge script script/SeamEmissionManagerV2Deploy.s.sol:SeamEmissionManagerV2Deploy --force --rpc-url tenderly --slow --broadcast -vvvv --verify --verifier-url ${TENDERLY_FORK_VERIFIER_URL} --etherscan-api-key ${TENDERLY_ACCESS_KEY}
29 changes: 29 additions & 0 deletions script/SeamEmissionManagerV2Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {SeamEmissionManagerV2} from "../src/SeamEmissionManagerV2.sol";
import {Constants} from "../src/library/Constants.sol";

contract SeamEmissionManagerV2Deploy is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployerAddress = vm.addr(deployerPrivateKey);

console.log("Deployer address: ", deployerAddress);
console.log("Deployer balance: ", deployerAddress.balance);
console.log("BlockNumber: ", block.number);
console.log("ChainId: ", block.chainid);

console.log("Deploying...");

vm.startBroadcast(deployerPrivateKey);

SeamEmissionManagerV2 implementation = new SeamEmissionManagerV2();

console.log("Deployed SeamEmissionManagerV2 implementation: ", address(implementation));

vm.stopBroadcast();
}
}
2 changes: 1 addition & 1 deletion src/SeamEmissionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ contract SeamEmissionManager is ISeamEmissionManager, Initializable, AccessContr
}

/// @inheritdoc ISeamEmissionManager
function claim(address receiver) external onlyRole(CLAIMER_ROLE) {
function claim(address receiver) external virtual onlyRole(CLAIMER_ROLE) {
Storage.Layout storage $ = Storage.layout();

uint64 emissionStartTimestamp = $.emissionStartTimestamp;
Expand Down
40 changes: 40 additions & 0 deletions src/SeamEmissionManagerV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {SeamEmissionManager} from "./SeamEmissionManager.sol";
import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import {SeamEmissionManagerStorage as Storage} from "./storage/SeamEmissionManagerStorage.sol";

/// @title SeamEmissionManagerV2
/// @author Seamless Protocol
/// @notice This contract is responsible for managing SEAM token emission.
contract SeamEmissionManagerV2 is SeamEmissionManager {
/// @inheritdoc SeamEmissionManager
function claim(address receiver) external override onlyRole(CLAIMER_ROLE) {
Storage.Layout storage $ = Storage.layout();

uint64 emissionStartTimestamp = $.emissionStartTimestamp;
if (emissionStartTimestamp > block.timestamp) {
revert EmissionsNotStarted(emissionStartTimestamp);
}

uint256 emissionPerSecond = $.emissionPerSecond;
uint64 lastClaimedTimestamp = $.lastClaimedTimestamp;
uint64 currentTimestamp = uint64(block.timestamp);
uint256 emissionAmount = (currentTimestamp - lastClaimedTimestamp) * emissionPerSecond;

// Check contract's SEAM balance and adjust emission amount if needed
// When emission amount exceeds balance, emission rate will not result
// in any more emissions until balance is increased (emission rate is not
// applied retroactively for time elapsed while no balance was available)
uint256 seamBalance = $.seam.balanceOf(address(this));
if (emissionAmount > seamBalance) {
emissionAmount = seamBalance;
}

SafeERC20.safeTransfer($.seam, receiver, emissionAmount);
$.lastClaimedTimestamp = currentTimestamp;

emit Claim(receiver, emissionAmount);
}
}
2 changes: 2 additions & 0 deletions src/library/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ library Constants {
address public constant GOVERNOR_LONG_ADDRESS = 0x04faA2826DbB38a7A4E9a5E3dB26b9E389E761B6;

uint256 public constant SEAM_EMISSION_PER_SECOND = 0.000000001 ether;
address public constant SEAM_EMISSION_MANAGER1_ADDRESS = 0x57460DC21bf1574b8e6E00D372b8Ca5Ec41b3955;
address public constant SEAM_EMISSION_MANAGER2_ADDRESS = 0x785c979EE8709060b3f71aEf4f2C09229DB90778;

address public constant INCENTIVES_CONTROLLER_ADDRESS = 0x91Ac2FfF8CBeF5859eAA6DdA661feBd533cD3780;

Expand Down
71 changes: 71 additions & 0 deletions test/fork/SeamEmissionManagerV2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {IERC20Errors} from "openzeppelin-contracts/interfaces/draft-IERC6093.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {SeamEmissionManager} from "../../src/SeamEmissionManager.sol";
import {SeamEmissionManagerV2} from "../../src/SeamEmissionManagerV2.sol";
import {Constants} from "../../src/library/Constants.sol";

contract SeamEmissionManagerV2ForkTest is Test {
IERC20 immutable SEAM = IERC20(Constants.SEAM_ADDRESS);
SeamEmissionManager emissionManager1 = SeamEmissionManager(Constants.SEAM_EMISSION_MANAGER1_ADDRESS);
SeamEmissionManager emissionManager2 = SeamEmissionManager(Constants.SEAM_EMISSION_MANAGER2_ADDRESS);

function setUp() public {
vm.createSelectFork(vm.envString("FORK_URL"), 31645245);
}

function testUpgrade() public {
address newImplementation = address(new SeamEmissionManagerV2());

vm.startPrank(Constants.SHORT_TIMELOCK_ADDRESS);

// Check that emission manager 1 reverts with insufficient balance. Since the vesting period is over
vm.expectPartialRevert(IERC20Errors.ERC20InsufficientBalance.selector);
emissionManager1.claim(address(this));

// Check that emission manager 2 claims successfully since the vesting period is not over
emissionManager2.claim(address(this));

vm.stopPrank();

// Store values before upgrade
address seamBefore1 = emissionManager1.getSeam();
uint64 emissionStartTimestampBefore1 = emissionManager1.getEmissionStartTimestamp();
uint64 lastClaimedTimestampBefore1 = emissionManager1.getLastClaimedTimestamp();
uint256 emissionPerSecondBefore1 = emissionManager1.getEmissionPerSecond();

address seamBefore2 = emissionManager2.getSeam();
uint64 emissionStartTimestampBefore2 = emissionManager2.getEmissionStartTimestamp();
uint64 lastClaimedTimestampBefore2 = emissionManager2.getLastClaimedTimestamp();
uint256 emissionPerSecondBefore2 = emissionManager2.getEmissionPerSecond();

vm.startPrank(Constants.LONG_TIMELOCK_ADDRESS);
emissionManager1.upgradeToAndCall(address(newImplementation), "");
emissionManager2.upgradeToAndCall(address(newImplementation), "");
vm.stopPrank();

// Validate values after upgrade match values before upgrade
assertEq(emissionManager1.getSeam(), seamBefore1);
assertEq(emissionManager1.getEmissionStartTimestamp(), emissionStartTimestampBefore1);
assertEq(emissionManager1.getLastClaimedTimestamp(), lastClaimedTimestampBefore1);
assertEq(emissionManager1.getEmissionPerSecond(), emissionPerSecondBefore1);

assertEq(emissionManager2.getSeam(), seamBefore2);
assertEq(emissionManager2.getEmissionStartTimestamp(), emissionStartTimestampBefore2);
assertEq(emissionManager2.getLastClaimedTimestamp(), lastClaimedTimestampBefore2);
assertEq(emissionManager2.getEmissionPerSecond(), emissionPerSecondBefore2);

vm.startPrank(Constants.SHORT_TIMELOCK_ADDRESS);
emissionManager1.claim(address(this));
emissionManager2.claim(address(this));

// Check that emission manager 1 claims successfully the full balance since the vesting period is over
assertEq(SEAM.balanceOf(address(emissionManager1)), 0);

vm.stopPrank();
}
}
185 changes: 185 additions & 0 deletions test/unit/SeamEmissionManagerV2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {ERC20Mock} from "openzeppelin-contracts/mocks/token/ERC20Mock.sol";
import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {SeamEmissionManagerV2} from "src/SeamEmissionManagerV2.sol";
import {SeamEmissionManager} from "src/SeamEmissionManager.sol";
import {ISeamEmissionManager} from "src/interfaces/ISeamEmissionManager.sol";
import {IAccessControl} from "openzeppelin-contracts/access/IAccessControl.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";

contract SeamEmissionManagerV2Test is Test {
address immutable seam = address(new ERC20Mock());
uint256 immutable emissionPerSecond = 1 ether;

SeamEmissionManagerV2 emissionManager;

function setUp() public {
SeamEmissionManagerV2 implementation = new SeamEmissionManagerV2();
ERC1967Proxy proxy = new ERC1967Proxy(
address(implementation),
abi.encodeWithSelector(
SeamEmissionManager.initialize.selector,
seam,
emissionPerSecond,
address(this),
address(this),
block.timestamp
)
);
emissionManager = SeamEmissionManagerV2(address(proxy));
}

function test_SetUp() public {
assertEq(emissionManager.getSeam(), seam);
assertEq(emissionManager.getEmissionPerSecond(), emissionPerSecond);
assertEq(emissionManager.getEmissionStartTimestamp(), block.timestamp);
assertEq(emissionManager.getLastClaimedTimestamp(), block.timestamp);
assertTrue(emissionManager.hasRole(emissionManager.DEFAULT_ADMIN_ROLE(), address(this)));
assertTrue(emissionManager.hasRole(emissionManager.CLAIMER_ROLE(), address(this)));
}

function test_SetEmissionStartTimestamp() public {
uint64 emissionStartTimestamp = uint64(block.timestamp) + 1;
emissionManager.setEmissionStartTimestamp(emissionStartTimestamp);
assertEq(emissionManager.getEmissionStartTimestamp(), emissionStartTimestamp);
assertEq(emissionManager.getLastClaimedTimestamp(), 0);
}

function testFuzz_SetEmissionStartTimestamp(uint64 emissionStartTimestamp) public {
emissionStartTimestamp = uint64(bound(emissionStartTimestamp, uint64(block.timestamp) + 1, type(uint64).max));
emissionManager.setEmissionStartTimestamp(emissionStartTimestamp);
assertEq(emissionManager.getEmissionStartTimestamp(), emissionStartTimestamp);
assertEq(emissionManager.getLastClaimedTimestamp(), 0);
}

function testFuzz_SetEmissionStartTimestamp_RevertIf_NotDefaultAdmin(address caller, uint48 emissionStartTimestamp)
public
{
vm.assume(caller != address(this));
vm.startPrank(caller);
vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector, caller, emissionManager.DEFAULT_ADMIN_ROLE()
)
);
emissionManager.setEmissionStartTimestamp(emissionStartTimestamp);
vm.stopPrank();
}

function test_SetEmissionPerSecond() public {
uint256 newEmissionPerSecond = 2 ether;
emissionManager.setEmissionPerSecond(newEmissionPerSecond);
assertEq(emissionManager.getEmissionPerSecond(), newEmissionPerSecond);
}

function testFuzz_SetEmissionPerSecond(uint256 newEmissionPerSecond) public {
emissionManager.setEmissionPerSecond(newEmissionPerSecond);
assertEq(emissionManager.getEmissionPerSecond(), newEmissionPerSecond);
}

function testFuzz_SetEmissionPerSecond_RevertIf_NotDefaultAdmin(address caller, uint256 newEmissionPerSecond)
public
{
vm.assume(caller != address(this));
vm.startPrank(caller);
vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector, caller, emissionManager.DEFAULT_ADMIN_ROLE()
)
);
emissionManager.setEmissionPerSecond(newEmissionPerSecond);
vm.stopPrank();
}

function test_Claim() public {
deal(seam, address(emissionManager), type(uint256).max);

uint256 receiverBalanceBefore = IERC20(seam).balanceOf(address(this));
uint256 emissionManagerBalanceBefore = IERC20(seam).balanceOf(address(emissionManager));

vm.warp(block.timestamp + 5000);
emissionManager.claim(address(this));

assertEq(IERC20(seam).balanceOf(address(this)), receiverBalanceBefore + emissionPerSecond * 5000);
assertEq(
IERC20(seam).balanceOf(address(emissionManager)), emissionManagerBalanceBefore - emissionPerSecond * 5000
);
}

function test_Claim_RevertIf_NotStarted() public {
deal(seam, address(emissionManager), type(uint256).max);

emissionManager.setEmissionStartTimestamp(uint64(block.timestamp) + 1);

vm.expectRevert(abi.encodeWithSelector(ISeamEmissionManager.EmissionsNotStarted.selector, block.timestamp + 1));
emissionManager.claim(address(this));
}

function test_Claim_InsufficientBalance() public {
uint256 timeElapsed = 5000;
uint256 partialAmount = emissionPerSecond * timeElapsed / 2; // Only half of the needed amount

// Set a specific token balance that's less than what would be required
deal(seam, address(emissionManager), partialAmount);

uint256 receiverBalanceBefore = IERC20(seam).balanceOf(address(this));

vm.warp(block.timestamp + timeElapsed);
emissionManager.claim(address(this));

// Check that only the available balance was transferred
assertEq(IERC20(seam).balanceOf(address(this)), receiverBalanceBefore + partialAmount);
assertEq(IERC20(seam).balanceOf(address(emissionManager)), 0);
}

function testFuzz_Claim(address receiver, uint256 timeElapsed) public {
vm.assume(receiver != address(0));
timeElapsed = bound(timeElapsed, 0, type(uint32).max / emissionPerSecond);
deal(seam, address(emissionManager), type(uint256).max);

uint256 receiverBalanceBefore = IERC20(seam).balanceOf(receiver);
uint256 emissionManagerBalanceBefore = IERC20(seam).balanceOf(address(emissionManager));

vm.warp(block.timestamp + timeElapsed);
emissionManager.claim(receiver);

assertEq(IERC20(seam).balanceOf(receiver), receiverBalanceBefore + emissionPerSecond * timeElapsed);
assertEq(
IERC20(seam).balanceOf(address(emissionManager)),
emissionManagerBalanceBefore - emissionPerSecond * timeElapsed
);
}

function testFuzz_Claim_InsufficientBalance(uint256 availableBalance) public {
address receiver = makeAddr("receiver");
uint32 timeElapsed = uint32(type(uint32).max);
uint256 requiredAmount = emissionPerSecond * timeElapsed;
availableBalance = bound(availableBalance, 1, requiredAmount - 1);

deal(seam, address(emissionManager), availableBalance);

uint256 receiverBalanceBefore = IERC20(seam).balanceOf(receiver);

vm.warp(block.timestamp + timeElapsed);
emissionManager.claim(receiver);

// Check that only the available balance was transferred
assertEq(IERC20(seam).balanceOf(receiver), receiverBalanceBefore + availableBalance);
assertEq(IERC20(seam).balanceOf(address(emissionManager)), 0);
}

function testFuzz_Claim_RevertIf_NotClaimer(address caller) public {
vm.assume(caller != address(this));
vm.startPrank(caller);
vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector, caller, emissionManager.CLAIMER_ROLE()
)
);
emissionManager.claim(address(this));
vm.stopPrank();
}
}