staking test cleanup
This commit is contained in:
parent
2b817c9331
commit
36833cab7f
10 changed files with 175 additions and 404 deletions
|
|
@ -7,13 +7,12 @@ import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
|
|||
import "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
||||
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
||||
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
||||
import "./interfaces/IStake.sol";
|
||||
import "./Harb.sol";
|
||||
|
||||
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
|
||||
error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare);
|
||||
|
||||
contract Stake is IStake {
|
||||
contract Stake {
|
||||
using Math for uint256;
|
||||
|
||||
uint256 internal DECIMAL_OFFSET = 5 + 2;
|
||||
|
|
@ -28,7 +27,7 @@ contract Stake is IStake {
|
|||
error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId);
|
||||
error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake);
|
||||
error NoPermission(address requester, address owner);
|
||||
error PositionNotFound();
|
||||
error PositionNotFound(uint256 positionId, address requester);
|
||||
|
||||
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 share, uint32 creationTime, uint32 taxRate);
|
||||
event TaxPaid(uint256 indexed positionId, address indexed owner, uint256 taxAmount);
|
||||
|
|
@ -58,23 +57,19 @@ contract Stake is IStake {
|
|||
nextPositionId = 654321;
|
||||
}
|
||||
|
||||
function dormantSupply() public view override returns (uint256) {
|
||||
return totalSupply * (100 - MAX_STAKE) / 100;
|
||||
}
|
||||
|
||||
function authorizedStake() private view returns (uint256) {
|
||||
return totalSupply * MAX_STAKE / 100;
|
||||
}
|
||||
|
||||
function assetsToShares(uint256 assets, Math.Rounding rounding) private view returns (uint256) {
|
||||
return assets.mulDiv(totalSupply, harb.totalSupply(), rounding);
|
||||
function assetsToShares(uint256 assets) public view returns (uint256) {
|
||||
return assets.mulDiv(totalSupply, harb.totalSupply(), Math.Rounding.Down);
|
||||
//return assets.mulDiv(totalSupply, harb.totalSupply() + 1, rounding);
|
||||
}
|
||||
|
||||
function sharesToAssets(uint256 shares, Math.Rounding rounding) private view returns (uint256) {
|
||||
function sharesToAssets(uint256 shares) public view returns (uint256) {
|
||||
//return shares.mulDiv(harb.totalSupply() + 1, totalSupply, rounding);
|
||||
// TODO: should the average total supply be used for this calculation?
|
||||
return shares.mulDiv(harb.totalSupply(), totalSupply, rounding);
|
||||
return shares.mulDiv(harb.totalSupply(), totalSupply, Math.Rounding.Down);
|
||||
}
|
||||
|
||||
function permitAndSnatch(
|
||||
|
|
@ -108,9 +103,9 @@ contract Stake is IStake {
|
|||
returns (uint256 positionId)
|
||||
{
|
||||
// check lower boundary
|
||||
uint256 sharesWanted = assetsToShares(assets, Math.Rounding.Down);
|
||||
uint256 sharesWanted = assetsToShares(assets);
|
||||
{
|
||||
// check that position size is multiple of minStake
|
||||
// check that position size is at least minStake
|
||||
// to prevent excessive fragmentation, increasing snatch cost
|
||||
uint256 minStake = harb.previousTotalSupply() / 3000;
|
||||
if (sharesWanted < minStake) {
|
||||
|
|
@ -127,8 +122,7 @@ contract Stake is IStake {
|
|||
for (uint256 i = 0; i < positionsToSnatch.length - 1; i++) {
|
||||
StakingPosition storage pos = positions[positionsToSnatch[i]];
|
||||
if (pos.creationTime == 0) {
|
||||
//TODO:
|
||||
revert PositionNotFound();
|
||||
revert PositionNotFound(positionsToSnatch[i], receiver);
|
||||
}
|
||||
// check that tax lower
|
||||
if (taxRate <= pos.taxRate) {
|
||||
|
|
@ -151,7 +145,7 @@ contract Stake is IStake {
|
|||
StakingPosition storage lastPos = positions[positionsToSnatch[index]];
|
||||
if (lastPos.creationTime == 0) {
|
||||
//TODO:
|
||||
revert PositionNotFound();
|
||||
revert PositionNotFound(positionsToSnatch[index], receiver);
|
||||
}
|
||||
// check that tax lower
|
||||
if (taxRate <= lastPos.taxRate) {
|
||||
|
|
@ -199,72 +193,76 @@ contract Stake is IStake {
|
|||
emit PositionCreated(positionId, sp.owner, sp.share, sp.creationTime, sp.taxRate);
|
||||
}
|
||||
|
||||
function changeTax(uint256 positionID, uint32 taxRate) public {
|
||||
function changeTax(uint256 positionId, uint32 taxRate) public {
|
||||
require(taxRate < TAX_RATES.length, "tax rate out of bounds");
|
||||
StakingPosition storage pos = positions[positionID];
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
if (pos.creationTime == 0) {
|
||||
revert PositionNotFound(positionId, msg.sender);
|
||||
}
|
||||
if (pos.owner != msg.sender) {
|
||||
revert NoPermission(msg.sender, pos.owner);
|
||||
}
|
||||
//TODO: implement not found
|
||||
// to prevent snatch-and-change grieving attack, pay TAX_FLOOR_DURATION
|
||||
require(taxRate > pos.taxRate, "tax too low to snatch");
|
||||
_payTax(positionID, pos, TAX_FLOOR_DURATION);
|
||||
_payTax(positionId, pos, TAX_FLOOR_DURATION);
|
||||
pos.taxRate = taxRate;
|
||||
}
|
||||
|
||||
function exitPosition(uint256 positionId) public {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
if (pos.creationTime == 0) {
|
||||
revert PositionNotFound(positionId, msg.sender);
|
||||
}
|
||||
if (pos.owner != msg.sender) {
|
||||
revert NoPermission(msg.sender, pos.owner);
|
||||
}
|
||||
//TODO: implement not found
|
||||
// to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION
|
||||
_payTax(positionId, pos, TAX_FLOOR_DURATION);
|
||||
_exitPosition(positionId, pos);
|
||||
}
|
||||
|
||||
// TODO: write a bot that calls this function regularly
|
||||
function payTax(uint256 positionID) public {
|
||||
StakingPosition storage pos = positions[positionID];
|
||||
function payTax(uint256 positionId) public {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
||||
_payTax(positionID, pos, 0);
|
||||
_payTax(positionId, pos, 0);
|
||||
}
|
||||
|
||||
function taxDue(uint256 positionID, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
|
||||
StakingPosition storage pos = positions[positionID];
|
||||
function taxDue(uint256 positionId, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
// ihet = Implied Holding Expiry Timestamp
|
||||
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
|
||||
? pos.creationTime + taxFloorDuration
|
||||
: block.timestamp;
|
||||
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
||||
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
|
||||
uint256 assetsBefore = sharesToAssets(pos.share);
|
||||
amountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
||||
}
|
||||
|
||||
function _payTax(uint256 positionID, StakingPosition storage pos, uint256 taxFloorDuration) private {
|
||||
function _payTax(uint256 positionId, StakingPosition storage pos, uint256 taxFloorDuration) private {
|
||||
// ihet = Implied Holding Expiry Timestamp
|
||||
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
|
||||
? pos.creationTime + taxFloorDuration
|
||||
: block.timestamp;
|
||||
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
||||
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
|
||||
uint256 assetsBefore = sharesToAssets(pos.share);
|
||||
uint256 taxAmountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
||||
if (taxAmountDue >= assetsBefore) {
|
||||
// can not pay more tax than value of position
|
||||
taxAmountDue = assetsBefore;
|
||||
}
|
||||
SafeERC20.safeTransfer(harb, taxPool, taxAmountDue);
|
||||
emit TaxPaid(positionID, pos.owner, taxAmountDue);
|
||||
emit TaxPaid(positionId, pos.owner, taxAmountDue);
|
||||
if (assetsBefore - taxAmountDue > 0) {
|
||||
// if something left over, update storage
|
||||
uint256 shareAfterTax = assetsToShares(assetsBefore - taxAmountDue, Math.Rounding.Down);
|
||||
uint256 shareAfterTax = assetsToShares(assetsBefore - taxAmountDue);
|
||||
outstandingStake -= pos.share - shareAfterTax;
|
||||
pos.share = shareAfterTax;
|
||||
pos.lastTaxTime = uint32(block.timestamp);
|
||||
} else {
|
||||
// if nothing left over, liquidate position
|
||||
outstandingStake -= pos.share;
|
||||
emit PositionRemoved(positionID, pos.share, pos.lastTaxTime);
|
||||
emit PositionRemoved(positionId, pos.share, pos.lastTaxTime);
|
||||
delete pos.owner;
|
||||
delete pos.creationTime;
|
||||
}
|
||||
|
|
@ -273,7 +271,7 @@ contract Stake is IStake {
|
|||
function _exitPosition(uint256 positionId, StakingPosition storage pos) private {
|
||||
outstandingStake -= pos.share;
|
||||
address owner = pos.owner;
|
||||
uint256 assets = sharesToAssets(pos.share, Math.Rounding.Down);
|
||||
uint256 assets = sharesToAssets(pos.share);
|
||||
emit PositionRemoved(positionId, pos.share, pos.lastTaxTime);
|
||||
delete pos.owner;
|
||||
delete pos.creationTime;
|
||||
|
|
@ -282,7 +280,7 @@ contract Stake is IStake {
|
|||
|
||||
function _shrinkPosition(uint256 positionId, StakingPosition storage pos, uint256 sharesToTake) private {
|
||||
require (sharesToTake < pos.share, "position too small");
|
||||
uint256 assets = sharesToAssets(sharesToTake, Math.Rounding.Down);
|
||||
uint256 assets = sharesToAssets(sharesToTake);
|
||||
pos.share -= sharesToTake;
|
||||
emit PositionShrunk(positionId, pos.share, pos.lastTaxTime, sharesToTake);
|
||||
SafeERC20.safeTransfer(harb, pos.owner, assets);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue