Compare commits
5 Commits
21c2002310
...
8e2caaba0f
Author | SHA1 | Date | |
---|---|---|---|
8e2caaba0f | |||
00a7dea8fb | |||
2a012ef8e2 | |||
162b3e357a | |||
62c70893a9 |
69
src/Gatekeeper.sol
Normal file
69
src/Gatekeeper.sol
Normal file
@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "./interfaces/IGatekeeper.sol";
|
||||
|
||||
contract Gatekeeper is IGatekeeper {
|
||||
uint256 public override ghostedSupply;
|
||||
address public immutable staking;
|
||||
|
||||
uint256 private _aggregatedPublicKey;
|
||||
mapping(uint256 => mapping(uint256 => bool)) private _executedTransaction;
|
||||
|
||||
constructor(
|
||||
address _staking,
|
||||
uint256 _ghostedSupply
|
||||
) {
|
||||
ghostedSupply = _ghostedSupply;
|
||||
staking = _staking;
|
||||
}
|
||||
|
||||
function ghost(bytes32 receiver, uint256 amount) external override {
|
||||
if (msg.sender != staking) revert NotStaking();
|
||||
ghostedSupply += amount;
|
||||
emit Ghosted(receiver, amount);
|
||||
}
|
||||
|
||||
function materialize(
|
||||
address receiver,
|
||||
uint256 amount,
|
||||
uint256 rx,
|
||||
uint256 s
|
||||
) external override {
|
||||
if (msg.sender != staking) revert NotStaking();
|
||||
_checkTransactionExistence(rx, s);
|
||||
bytes32 message = keccak256(abi.encodePacked("materialize", receiver, amount));
|
||||
|
||||
if (_incorrectSignature(rx, s, message)) revert WrongSignature();
|
||||
ghostedSupply -= amount;
|
||||
emit Materialized(receiver, amount);
|
||||
}
|
||||
|
||||
function rotate(
|
||||
uint256 aggregatedPublicKey,
|
||||
uint256 rx,
|
||||
uint256 s
|
||||
) external override {
|
||||
_checkTransactionExistence(rx, s);
|
||||
bytes32 message = keccak256(abi.encodePacked("rotate", aggregatedPublicKey));
|
||||
|
||||
if (_incorrectSignature(rx, s, message)) revert WrongSignature();
|
||||
_aggregatedPublicKey = aggregatedPublicKey;
|
||||
emit Rotated(aggregatedPublicKey);
|
||||
}
|
||||
|
||||
function _checkTransactionExistence(uint256 rx, uint256 s) private {
|
||||
if (_executedTransaction[rx][s]) revert AlreadyExecuted();
|
||||
_executedTransaction[rx][s] = true;
|
||||
}
|
||||
|
||||
function _incorrectSignature(uint256 rx, uint256 s, bytes32 m) private pure returns (bool) {
|
||||
// no logic below, just to suppress warnings from solc
|
||||
uint256 void = rx;
|
||||
void = s;
|
||||
void = uint256(m);
|
||||
|
||||
// always bad signature for now
|
||||
return true;
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import "./interfaces/ISTNK.sol";
|
||||
import "./interfaces/IGHST.sol";
|
||||
import "./interfaces/IDistributor.sol";
|
||||
import "./interfaces/IStaking.sol";
|
||||
import "./interfaces/IGatekeeper.sol";
|
||||
|
||||
contract GhostStaking is IStaking, GhostAccessControlled {
|
||||
using SafeERC20 for IERC20;
|
||||
@ -24,6 +25,7 @@ contract GhostStaking is IStaking, GhostAccessControlled {
|
||||
|
||||
Epoch public epoch;
|
||||
address public distributor;
|
||||
address public gatekeeper;
|
||||
uint256 public sharesInWarmup;
|
||||
|
||||
mapping(address => Claim) public warmupInfo;
|
||||
@ -133,6 +135,24 @@ contract GhostStaking is IStaking, GhostAccessControlled {
|
||||
ISTNK(stnk).safeTransfer(to, balance);
|
||||
}
|
||||
|
||||
function ghost(
|
||||
bytes32 receiver,
|
||||
uint256 amount
|
||||
) external override {
|
||||
IGHST(ghst).burn(msg.sender, amount);
|
||||
IGatekeeper(gatekeeper).ghost(receiver, amount);
|
||||
}
|
||||
|
||||
function materialize(
|
||||
address receiver,
|
||||
uint256 amount,
|
||||
uint256 rx,
|
||||
uint256 s
|
||||
) external override {
|
||||
IGatekeeper(gatekeeper).materialize(receiver, amount, rx, s);
|
||||
IGHST(ghst).mint(receiver, amount);
|
||||
}
|
||||
|
||||
function rebase() public override returns (uint256 bounty) {
|
||||
if (epoch.end <= block.timestamp) {
|
||||
ISTNK(stnk).rebase(epoch.distribute, epoch.number);
|
||||
@ -164,6 +184,11 @@ contract GhostStaking is IStaking, GhostAccessControlled {
|
||||
emit WarmupSet(_warmupPeriod);
|
||||
}
|
||||
|
||||
function setGatekeeperAddress(address _gatekeeper) external onlyGovernor {
|
||||
gatekeeper = _gatekeeper;
|
||||
emit GatekeeperSet(_gatekeeper);
|
||||
}
|
||||
|
||||
function index() public view override returns (uint256) {
|
||||
return ISTNK(stnk).index();
|
||||
}
|
||||
@ -172,6 +197,12 @@ contract GhostStaking is IStaking, GhostAccessControlled {
|
||||
return ISTNK(stnk).balanceForShares(sharesInWarmup);
|
||||
}
|
||||
|
||||
function ghostedSupply() public view override returns (uint256 amount) {
|
||||
if (gatekeeper != address(0)) {
|
||||
amount = IGatekeeper(gatekeeper).ghostedSupply();
|
||||
}
|
||||
}
|
||||
|
||||
function _send(
|
||||
uint256 amount,
|
||||
address to,
|
||||
|
@ -168,7 +168,7 @@ contract Stinky is ISTNK, ERC20Permit {
|
||||
|
||||
function circulatingSupply() public view override returns (uint256) {
|
||||
return _totalSupply +
|
||||
IGHST(ghst).balanceFrom(IERC20(ghst).totalSupply()) +
|
||||
IGHST(ghst).balanceFrom(IERC20(ghst).totalSupply() + IStaking(staking).ghostedSupply()) +
|
||||
IStaking(staking).supplyInWarmup() -
|
||||
balanceOf(staking);
|
||||
}
|
||||
|
17
src/interfaces/IGatekeeper.sol
Normal file
17
src/interfaces/IGatekeeper.sol
Normal file
@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
interface IGatekeeper {
|
||||
error NotStaking();
|
||||
error WrongSignature();
|
||||
error AlreadyExecuted();
|
||||
|
||||
event Ghosted(bytes32 indexed receiver, uint256 indexed amount);
|
||||
event Materialized(address indexed receiver, uint256 indexed amount);
|
||||
event Rotated(uint256 indexed aggregatedPublicKey);
|
||||
|
||||
function ghostedSupply() external view returns (uint256);
|
||||
function ghost(bytes32 receiver, uint256 amount) external;
|
||||
function materialize(address receiver, uint256 amount, uint256 rx, uint256 s) external;
|
||||
function rotate(uint256 aggregatedPublicKey, uint256 rx, uint256 s) external;
|
||||
}
|
@ -7,6 +7,7 @@ interface IStaking {
|
||||
error InsufficientBalance();
|
||||
|
||||
event DistributorSet(address distributor);
|
||||
event GatekeeperSet(address gatekeeper);
|
||||
event WarmupSet(uint256 warmup);
|
||||
|
||||
struct Epoch {
|
||||
@ -43,7 +44,11 @@ interface IStaking {
|
||||
|
||||
function wrap(address _to, uint256 _amount) external returns (uint256 gBalance_);
|
||||
function unwrap(address _to, uint256 _amount) external returns (uint256 sBalance_);
|
||||
function ghost(bytes32 receiver, uint256 amount) external;
|
||||
function materialize(address receiver, uint256 amount, uint256 rx, uint256 s) external;
|
||||
function rebase() external returns (uint256);
|
||||
|
||||
function index() external view returns (uint256);
|
||||
function supplyInWarmup() external view returns (uint256);
|
||||
function ghostedSupply() external view returns (uint256);
|
||||
}
|
||||
|
67
test/gatekeeper/Gatekeeper.t.sol
Normal file
67
test/gatekeeper/Gatekeeper.t.sol
Normal file
@ -0,0 +1,67 @@
|
||||
pragma solidity 0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
|
||||
import "../../src/Gatekeeper.sol";
|
||||
|
||||
contract GatekeeperTest is Test {
|
||||
address constant alice = 0x0000000000000000000000000000000000000001;
|
||||
address constant bob = 0x0000000000000000000000000000000000000002;
|
||||
uint256 constant initAmount = 69 * 1e18;
|
||||
Gatekeeper gatekeeper;
|
||||
|
||||
event Ghosted(bytes32 indexed receiver, uint256 indexed amount);
|
||||
|
||||
function setUp() public {
|
||||
gatekeeper = new Gatekeeper(alice, 0);
|
||||
}
|
||||
|
||||
function test_correctInitialization() public {
|
||||
assertEq(gatekeeper.staking(), alice);
|
||||
assertEq(gatekeeper.ghostedSupply(), 0);
|
||||
|
||||
Gatekeeper anotherGatekeeper = new Gatekeeper(bob, initAmount);
|
||||
assertEq(anotherGatekeeper.staking(), bob);
|
||||
assertEq(anotherGatekeeper.ghostedSupply(), initAmount);
|
||||
}
|
||||
|
||||
function test_ghostTokensWork(uint256 ghostAmount) public {
|
||||
vm.assume(ghostAmount > 0);
|
||||
bytes32 receiver = bytes32(abi.encodePacked(alice));
|
||||
uint256 ghostedSupply = gatekeeper.ghostedSupply();
|
||||
|
||||
vm.prank(alice);
|
||||
gatekeeper.ghost(receiver, ghostAmount);
|
||||
assertEq(gatekeeper.ghostedSupply(), ghostedSupply + ghostAmount);
|
||||
}
|
||||
|
||||
function test_couldNotGhostTokensFromArbitraryAddress(address someone) public {
|
||||
vm.assume(someone != alice);
|
||||
bytes32 receiver = bytes32(abi.encodePacked(alice));
|
||||
|
||||
vm.expectRevert();
|
||||
vm.prank(someone);
|
||||
gatekeeper.ghost(receiver, 69);
|
||||
assertEq(gatekeeper.ghostedSupply(), 0);
|
||||
}
|
||||
|
||||
function test_ghostTokensEmitsEvent(uint256 ghostAmount) public {
|
||||
vm.assume(ghostAmount > 0);
|
||||
bytes32 receiver = bytes32(abi.encodePacked(alice));
|
||||
|
||||
vm.expectEmit(true, true, true, false, address(gatekeeper));
|
||||
emit Ghosted(receiver, ghostAmount);
|
||||
vm.prank(alice);
|
||||
gatekeeper.ghost(receiver, ghostAmount);
|
||||
}
|
||||
|
||||
function test_materializeWork(uint256 ghostAmount) public {
|
||||
vm.expectRevert();
|
||||
gatekeeper.materialize(alice, ghostAmount, 0, 0);
|
||||
}
|
||||
|
||||
function test_rotateWork(uint256 aggregatedPublicKey) public {
|
||||
vm.expectRevert();
|
||||
gatekeeper.rotate(aggregatedPublicKey, 0, 0);
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import "../../src/StakingDistributor.sol";
|
||||
import "../../src/Treasury.sol";
|
||||
import "../../src/Staking.sol";
|
||||
import "../../src/mocks/ERC20Mock.sol";
|
||||
import "../../src/Gatekeeper.sol";
|
||||
|
||||
contract StakingTest is Test {
|
||||
address constant initializer = 0x0000000000000000000000000000000000000001;
|
||||
@ -33,11 +34,14 @@ contract StakingTest is Test {
|
||||
GhostStaking staking;
|
||||
GhostTreasury treasury;
|
||||
GhostAuthority authority;
|
||||
Gatekeeper gatekeeper;
|
||||
|
||||
uint256 public constant amount = 69;
|
||||
uint256 public constant bigAmount = amount * 1e9;
|
||||
|
||||
event DistributorSet(address distributor);
|
||||
event WarmupSet(uint256 warmup);
|
||||
event Ghosted(bytes32 indexed receiver, uint256 indexed amount);
|
||||
|
||||
function setUp() public {
|
||||
vm.startPrank(initializer);
|
||||
@ -62,6 +66,7 @@ contract StakingTest is Test {
|
||||
treasury = new GhostTreasury(address(ftso), 69, address(authority));
|
||||
stnk.initialize(address(staking), address(treasury), address(ghst));
|
||||
ghst.initialize(address(staking));
|
||||
gatekeeper = new Gatekeeper(address(staking), 0);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
@ -507,6 +512,96 @@ contract StakingTest is Test {
|
||||
assertEq(ftso.balanceOf(address(staking)), 2 * postBounty + bounty);
|
||||
}
|
||||
|
||||
function test_arbitraryAddressCouldNotAddGatekeeper(address someone, address maybeGatekeeper) public {
|
||||
vm.assume(maybeGatekeeper != address(0) && someone != governor);
|
||||
vm.expectRevert();
|
||||
vm.prank(someone);
|
||||
staking.setDistributor(maybeGatekeeper);
|
||||
}
|
||||
|
||||
function test_governorCouldSetGatekeeper(address maybeGatekeeper) public {
|
||||
vm.assume(maybeGatekeeper != address(0));
|
||||
assertEq(staking.gatekeeper(), address(0));
|
||||
vm.prank(governor);
|
||||
staking.setGatekeeperAddress(maybeGatekeeper);
|
||||
assertEq(staking.gatekeeper(), maybeGatekeeper);
|
||||
}
|
||||
|
||||
function test_couldNotGhostIfNoGatekeeper() public {
|
||||
assertEq(staking.ghostedSupply(), 0);
|
||||
vm.expectRevert();
|
||||
staking.ghost(bytes32(abi.encodePacked(alice)), amount);
|
||||
assertEq(staking.ghostedSupply(), 0);
|
||||
}
|
||||
|
||||
function test_couldNotMaterializeIfNoGatekeeper() public {
|
||||
assertEq(staking.ghostedSupply(), 0);
|
||||
vm.expectRevert();
|
||||
staking.materialize(alice, amount, 0, 0); // dummy rx and s
|
||||
assertEq(staking.ghostedSupply(), 0);
|
||||
}
|
||||
|
||||
function test_couldNotGhostTokensIfNoGhst() public {
|
||||
assertEq(staking.gatekeeper(), address(0));
|
||||
vm.prank(governor);
|
||||
staking.setGatekeeperAddress(address(gatekeeper));
|
||||
assertEq(staking.gatekeeper(), address(gatekeeper));
|
||||
|
||||
assertEq(staking.ghostedSupply(), 0);
|
||||
vm.expectRevert();
|
||||
vm.prank(alice);
|
||||
staking.ghost(bytes32(abi.encodePacked(alice)), amount);
|
||||
assertEq(staking.ghostedSupply(), 0);
|
||||
}
|
||||
|
||||
function test_correctlyGhostTokens() public {
|
||||
assertEq(staking.gatekeeper(), address(0));
|
||||
vm.prank(governor);
|
||||
staking.setGatekeeperAddress(address(gatekeeper));
|
||||
assertEq(staking.gatekeeper(), address(gatekeeper));
|
||||
|
||||
_prepareAndRoll(alice, bigAmount, true, true);
|
||||
uint256 aliceBalance = stnk.balanceOf(alice);
|
||||
|
||||
vm.startPrank(alice);
|
||||
stnk.approve(address(staking), aliceBalance);
|
||||
uint256 ghstBalance = staking.wrap(alice, aliceBalance);
|
||||
vm.stopPrank();
|
||||
|
||||
assertEq(staking.ghostedSupply(), 0);
|
||||
assertEq(stnk.circulatingSupply(), bigAmount - 1); // precision fix
|
||||
assertEq(ghst.totalSupply(), ghstBalance);
|
||||
|
||||
vm.prank(alice);
|
||||
staking.ghost(bytes32(abi.encodePacked(alice)), ghstBalance);
|
||||
|
||||
assertEq(staking.ghostedSupply(), ghstBalance);
|
||||
assertEq(stnk.circulatingSupply(), bigAmount - 1); // precision fix
|
||||
assertEq(ghst.totalSupply(), 0);
|
||||
}
|
||||
|
||||
function test_ghostTokensEmitsEvent() public {
|
||||
assertEq(staking.gatekeeper(), address(0));
|
||||
vm.prank(governor);
|
||||
staking.setGatekeeperAddress(address(gatekeeper));
|
||||
assertEq(staking.gatekeeper(), address(gatekeeper));
|
||||
|
||||
_prepareAndRoll(alice, bigAmount, true, true);
|
||||
uint256 aliceBalance = stnk.balanceOf(alice);
|
||||
|
||||
vm.startPrank(alice);
|
||||
stnk.approve(address(staking), aliceBalance);
|
||||
uint256 ghstBalance = staking.wrap(alice, aliceBalance);
|
||||
vm.stopPrank();
|
||||
|
||||
bytes32 receiver = bytes32(abi.encodePacked(alice));
|
||||
vm.expectEmit(true, true, true, false, address(gatekeeper));
|
||||
emit Ghosted(receiver, ghstBalance);
|
||||
|
||||
vm.prank(alice);
|
||||
staking.ghost(receiver, ghstBalance);
|
||||
}
|
||||
|
||||
function _mintAndApprove(address who, uint256 value) internal {
|
||||
vm.prank(vault);
|
||||
ftso.mint(who, value);
|
||||
|
Loading…
Reference in New Issue
Block a user