-
Notifications
You must be signed in to change notification settings - Fork 263
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
1,022 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright 2024 Aztec Labs. | ||
pragma solidity >=0.8.27; | ||
|
||
import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; | ||
|
||
// None -> Does not exist in our setup | ||
// Validating -> Participating as validator | ||
// Living -> Not participating as validator, but have funds in setup, | ||
// hit if slashes and going below the minimum | ||
// Exiting -> In the process of exiting the system | ||
enum Status { | ||
NONE, | ||
VALIDATING, | ||
LIVING, | ||
EXITING | ||
} | ||
|
||
struct ValidatorInfo { | ||
uint256 stake; | ||
address withdrawer; | ||
address proposer; | ||
Status status; | ||
} | ||
|
||
struct OperatorInfo { | ||
address proposer; | ||
address attester; | ||
} | ||
|
||
struct Exit { | ||
Timestamp exitableAt; | ||
address recipient; | ||
} | ||
|
||
interface IStaking { | ||
event Deposit( | ||
address indexed attester, address indexed proposer, address indexed withdrawer, uint256 amount | ||
); | ||
event WithdrawInitiated(address indexed attester, address indexed recipient, uint256 amount); | ||
event WithdrawFinalised(address indexed attester, address indexed recipient, uint256 amount); | ||
event Slashed(address indexed attester, uint256 amount); | ||
|
||
function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) | ||
external; | ||
function initiateWithdraw(address _attester, address _recipient) external returns (bool); | ||
function finaliseWithdraw(address _attester) external; | ||
function slash(address _attester, uint256 _amount) external; | ||
|
||
function getInfo(address _attester) external view returns (ValidatorInfo memory); | ||
function getExit(address _attester) external view returns (Exit memory); | ||
function getActiveAttesterCount() external view returns (uint256); | ||
function getAttesterAtIndex(uint256 _index) external view returns (address); | ||
function getProposerAtIndex(uint256 _index) external view returns (address); | ||
function getProposerForAttester(address _attester) external view returns (address); | ||
function getOperatorAtIndex(uint256 _index) external view returns (OperatorInfo memory); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright 2024 Aztec Labs. | ||
pragma solidity >=0.8.27; | ||
|
||
import { | ||
IStaking, ValidatorInfo, Exit, Status, OperatorInfo | ||
} from "@aztec/core/interfaces/IStaking.sol"; | ||
import {Errors} from "@aztec/core/libraries/Errors.sol"; | ||
import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; | ||
import {IERC20} from "@oz/token/ERC20/IERC20.sol"; | ||
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; | ||
import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; | ||
|
||
contract Staking is IStaking { | ||
using SafeERC20 for IERC20; | ||
using EnumerableSet for EnumerableSet.AddressSet; | ||
|
||
// Constant pulled out of the ass | ||
Timestamp public constant EXIT_DELAY = Timestamp.wrap(60 * 60 * 24); | ||
|
||
address public immutable SLASHER; | ||
IERC20 public immutable STAKING_ASSET; | ||
uint256 public immutable MINIMUM_STAKE; | ||
|
||
// address <=> index | ||
EnumerableSet.AddressSet internal attesters; | ||
|
||
mapping(address attester => ValidatorInfo) internal info; | ||
mapping(address attester => Exit) internal exits; | ||
|
||
constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake) { | ||
SLASHER = _slasher; | ||
STAKING_ASSET = _stakingAsset; | ||
MINIMUM_STAKE = _minimumStake; | ||
} | ||
|
||
function finaliseWithdraw(address _attester) external override(IStaking) { | ||
ValidatorInfo storage validator = info[_attester]; | ||
require(validator.status == Status.EXITING, Errors.Staking__NotExiting(_attester)); | ||
|
||
Exit storage exit = exits[_attester]; | ||
require( | ||
exit.exitableAt <= Timestamp.wrap(block.timestamp), | ||
Errors.Staking__WithdrawalNotUnlockedYet(Timestamp.wrap(block.timestamp), exit.exitableAt) | ||
); | ||
|
||
uint256 amount = validator.stake; | ||
address recipient = exit.recipient; | ||
|
||
delete exits[_attester]; | ||
delete info[_attester]; | ||
|
||
STAKING_ASSET.transfer(recipient, amount); | ||
|
||
emit IStaking.WithdrawFinalised(_attester, recipient, amount); | ||
} | ||
|
||
function slash(address _attester, uint256 _amount) external override(IStaking) { | ||
require(msg.sender == SLASHER, Errors.Staking__NotSlasher(SLASHER, msg.sender)); | ||
|
||
ValidatorInfo storage validator = info[_attester]; | ||
require(validator.status != Status.NONE, Errors.Staking__NoOneToSlash(_attester)); | ||
|
||
// There is a special, case, if exiting and past the limit, it is untouchable! | ||
require( | ||
!( | ||
validator.status == Status.EXITING | ||
&& exits[_attester].exitableAt <= Timestamp.wrap(block.timestamp) | ||
), | ||
Errors.Staking__CannotSlashExitedStake(_attester) | ||
); | ||
validator.stake -= _amount; | ||
|
||
// If the attester was validating AND is slashed below the MINIMUM_STAKE we update him to LIVING | ||
// When LIVING, he can only start exiting, we don't "really" exit him, because that cost | ||
// gas and cost edge cases around recipient, so lets just avoid that. | ||
if (validator.status == Status.VALIDATING && validator.stake < MINIMUM_STAKE) { | ||
require(attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); | ||
validator.status = Status.LIVING; | ||
} | ||
|
||
emit Slashed(_attester, _amount); | ||
} | ||
|
||
function getInfo(address _attester) | ||
external | ||
view | ||
override(IStaking) | ||
returns (ValidatorInfo memory) | ||
{ | ||
return info[_attester]; | ||
} | ||
|
||
function getProposerForAttester(address _attester) | ||
external | ||
view | ||
override(IStaking) | ||
returns (address) | ||
{ | ||
return info[_attester].proposer; | ||
} | ||
|
||
function getExit(address _attester) external view override(IStaking) returns (Exit memory) { | ||
return exits[_attester]; | ||
} | ||
|
||
function getAttesterAtIndex(uint256 _index) external view override(IStaking) returns (address) { | ||
return attesters.at(_index); | ||
} | ||
|
||
function getProposerAtIndex(uint256 _index) external view override(IStaking) returns (address) { | ||
return info[attesters.at(_index)].proposer; | ||
} | ||
|
||
function getOperatorAtIndex(uint256 _index) | ||
external | ||
view | ||
override(IStaking) | ||
returns (OperatorInfo memory) | ||
{ | ||
address attester = attesters.at(_index); | ||
return OperatorInfo({proposer: info[attester].proposer, attester: attester}); | ||
} | ||
|
||
function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) | ||
public | ||
virtual | ||
override(IStaking) | ||
{ | ||
require(_amount >= MINIMUM_STAKE, Errors.Staking__InsufficientStake(_amount, MINIMUM_STAKE)); | ||
STAKING_ASSET.transferFrom(msg.sender, address(this), _amount); | ||
require(info[_attester].status == Status.NONE, Errors.Staking__AlreadyRegistered(_attester)); | ||
require(attesters.add(_attester), Errors.Staking__AlreadyActive(_attester)); | ||
|
||
// If BLS, need to check possession of private key to avoid attacks. | ||
|
||
info[_attester] = ValidatorInfo({ | ||
stake: _amount, | ||
withdrawer: _withdrawer, | ||
proposer: _proposer, | ||
status: Status.VALIDATING | ||
}); | ||
|
||
emit IStaking.Deposit(_attester, _proposer, _withdrawer, _amount); | ||
} | ||
|
||
function initiateWithdraw(address _attester, address _recipient) | ||
public | ||
virtual | ||
override(IStaking) | ||
returns (bool) | ||
{ | ||
ValidatorInfo storage validator = info[_attester]; | ||
|
||
require( | ||
msg.sender == validator.withdrawer, | ||
Errors.Staking__NotWithdrawer(validator.withdrawer, msg.sender) | ||
); | ||
require( | ||
validator.status == Status.VALIDATING || validator.status == Status.LIVING, | ||
Errors.Staking__NothingToExit(_attester) | ||
); | ||
if (validator.status == Status.VALIDATING) { | ||
require(attesters.remove(_attester), Errors.Staking__FailedToRemove(_attester)); | ||
} | ||
|
||
// Note that the "amount" is not stored here, but reusing the `validators` | ||
// We always exit fully. | ||
exits[_attester] = | ||
Exit({exitableAt: Timestamp.wrap(block.timestamp) + EXIT_DELAY, recipient: _recipient}); | ||
validator.status = Status.EXITING; | ||
|
||
emit IStaking.WithdrawInitiated(_attester, _recipient, validator.stake); | ||
|
||
return true; | ||
} | ||
|
||
function getActiveAttesterCount() public view override(IStaking) returns (uint256) { | ||
return attesters.length(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright 2024 Aztec Labs. | ||
pragma solidity >=0.8.27; | ||
|
||
import {Staking, Status} from "@aztec/core/staking/Staking.sol"; | ||
import {IERC20} from "@oz/token/ERC20/IERC20.sol"; | ||
import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; | ||
|
||
contract StakingCheater is Staking { | ||
using EnumerableSet for EnumerableSet.AddressSet; | ||
|
||
constructor(address _slasher, IERC20 _stakingAsset, uint256 _minimumStake) | ||
Staking(_slasher, _stakingAsset, _minimumStake) | ||
{} | ||
|
||
function cheat__SetStatus(address _attester, Status _status) external { | ||
info[_attester].status = _status; | ||
} | ||
|
||
function cheat__AddAttester(address _attester) external { | ||
attesters.add(_attester); | ||
} | ||
|
||
function cheat__RemoveAttester(address _attester) external { | ||
attesters.remove(_attester); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity >=0.8.27; | ||
|
||
import {TestBase} from "@test/base/Base.sol"; | ||
|
||
import {StakingCheater} from "./StakingCheater.sol"; | ||
import {TestERC20} from "@aztec/mock/TestERC20.sol"; | ||
|
||
contract StakingBase is TestBase { | ||
StakingCheater internal staking; | ||
TestERC20 internal stakingAsset; | ||
|
||
uint256 internal constant MINIMUM_STAKE = 100e18; | ||
|
||
address internal constant PROPOSER = address(bytes20("PROPOSER")); | ||
address internal constant ATTESTER = address(bytes20("ATTESTER")); | ||
address internal constant WITHDRAWER = address(bytes20("WITHDRAWER")); | ||
address internal constant RECIPIENT = address(bytes20("RECIPIENT")); | ||
address internal constant SLASHER = address(bytes20("SLASHER")); | ||
|
||
function setUp() public virtual { | ||
stakingAsset = new TestERC20(); | ||
staking = new StakingCheater(SLASHER, stakingAsset, MINIMUM_STAKE); | ||
} | ||
} |
Oops, something went wrong.