diff --git a/package.json b/package.json index 2f8b760..7d0c4fe 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ }, "scripts": { "clean": "rimraf cache out", - "lint": "yarn lint:sol && yarn prettier:check", - "lint:sol": "forge fmt --check && yarn solhint \"{src,test}/**/*.sol\"", + "lint": "yarn lint:sol && yarn prettier:write", + "lint:sol": "yarn solhint \"{src,test,script}/**/*.sol\"", "postinstall": "husky install", "prettier:check": "prettier --check \"**/*.{json,md,yml}\"", "prettier:write": "prettier --write \"**/*.{json,md,yml}\"" diff --git a/script/Adapter_Deploy.s.sol b/script/Adapter_Deploy.s.sol index af2e66f..08cd8b0 100644 --- a/script/Adapter_Deploy.s.sol +++ b/script/Adapter_Deploy.s.sol @@ -9,6 +9,8 @@ // // Copyright (c) Tenderize Labs Ltd +// solhint-disable no-console + pragma solidity >=0.8.19; import { Script } from "forge-std/Script.sol"; diff --git a/script/Tenderize_Deploy.s.sol b/script/Tenderize_Deploy.s.sol index 5b7942c..172b206 100644 --- a/script/Tenderize_Deploy.s.sol +++ b/script/Tenderize_Deploy.s.sol @@ -9,6 +9,8 @@ // // Copyright (c) Tenderize Labs Ltd +// solhint-disable no-console + pragma solidity >=0.8.19; import { Script, console2 } from "forge-std/Script.sol"; diff --git a/script/XYZ_Data.s.sol b/script/XYZ_Data.s.sol index 2adb2c0..a57c089 100644 --- a/script/XYZ_Data.s.sol +++ b/script/XYZ_Data.s.sol @@ -9,6 +9,8 @@ // // Copyright (c) Tenderize Labs Ltd +// solhint-disable no-console + pragma solidity >=0.8.19; import { Script, console2 } from "forge-std/Script.sol"; diff --git a/script/XYZ_Deploy.s.sol b/script/XYZ_Deploy.s.sol index 81cb483..a278e2a 100644 --- a/script/XYZ_Deploy.s.sol +++ b/script/XYZ_Deploy.s.sol @@ -9,6 +9,8 @@ // // Copyright (c) Tenderize Labs Ltd +// solhint-disable no-console + pragma solidity >=0.8.19; import { Script, console2 } from "forge-std/Script.sol"; diff --git a/script/XYZ_Faucet.s.sol b/script/XYZ_Faucet.s.sol index 5d5b0e9..ef08244 100644 --- a/script/XYZ_Faucet.s.sol +++ b/script/XYZ_Faucet.s.sol @@ -9,6 +9,8 @@ // // Copyright (c) Tenderize Labs Ltd +// solhint-disable no-console + pragma solidity >=0.8.19; import { Script, console2 } from "forge-std/Script.sol"; diff --git a/src/adapters/Adapter.sol b/src/adapters/Adapter.sol index 587351d..39ea70a 100644 --- a/src/adapters/Adapter.sol +++ b/src/adapters/Adapter.sol @@ -20,6 +20,10 @@ interface Adapter is IERC165 { function unlockMaturity(uint256 unlockID) external view returns (uint256); + function unlockTime() external view returns (uint256); + + function currentTime() external view returns (uint256); + function stake(address validator, uint256 amount) external; function unstake(address validator, uint256 amount) external returns (uint256 unlockID); diff --git a/src/adapters/GraphAdapter.sol b/src/adapters/GraphAdapter.sol index 6b32447..5055b59 100644 --- a/src/adapters/GraphAdapter.sol +++ b/src/adapters/GraphAdapter.sol @@ -92,6 +92,14 @@ contract GraphAdapter is Adapter { } } + function unlockTime() external view override returns (uint256) { + return GRAPH.thawingPeriod(); + } + + function currentTime() external view override returns (uint256) { + return block.number; + } + function stake(address validator, uint256 amount) external override { GRT.safeApprove(address(GRAPH), amount); GRAPH.delegate(validator, amount); diff --git a/src/adapters/LivepeerAdapter.sol b/src/adapters/LivepeerAdapter.sol index 954e34b..db7f5e4 100644 --- a/src/adapters/LivepeerAdapter.sol +++ b/src/adapters/LivepeerAdapter.sol @@ -79,6 +79,14 @@ contract LivepeerAdapter is Adapter { } } + function unlockTime() external view override returns (uint256) { + return LIVEPEER_ROUNDS.roundLength() * LIVEPEER.unbondingPeriod(); + } + + function currentTime() external view override returns (uint256) { + return block.number; + } + function stake(address validator, uint256 amount) public { LPT.approve(address(LIVEPEER), amount); LIVEPEER.bond(amount, validator); diff --git a/src/adapters/PolygonAdapter.sol b/src/adapters/PolygonAdapter.sol index 8a273ae..8cdc339 100644 --- a/src/adapters/PolygonAdapter.sol +++ b/src/adapters/PolygonAdapter.sol @@ -77,6 +77,14 @@ contract PolygonAdapter is Adapter { return u.withdrawEpoch + WITHDRAW_DELAY; } + function unlockTime() external pure override returns (uint256) { + return WITHDRAW_DELAY; + } + + function currentTime() external view override returns (uint256) { + return MATIC_STAKE_MANAGER.epoch(); + } + function stake(address validator, uint256 amount) external override { // approve tokens POLY.safeApprove(address(MATIC_STAKE_MANAGER), amount); @@ -128,6 +136,7 @@ contract PolygonAdapter is Adapter { // This call will revert if there are no rewards // In which case we don't throw, just return the current staked amount. + // solhint-disable-next-line no-empty-blocks try validatorShares.restake() { } catch { return currentStake; diff --git a/src/adapters/interfaces/ILivepeer.sol b/src/adapters/interfaces/ILivepeer.sol index 46c1f59..06ea70a 100644 --- a/src/adapters/interfaces/ILivepeer.sol +++ b/src/adapters/interfaces/ILivepeer.sol @@ -46,6 +46,8 @@ interface ILivepeerBondingManager { returns (uint256 amount, uint256 withdrawRound); function isRegisteredTranscoder(address _transcoder) external view returns (bool); + + function unbondingPeriod() external view returns (uint256); } interface ILivepeerRoundsManager { diff --git a/src/adapters/interfaces/IPolygon.sol b/src/adapters/interfaces/IPolygon.sol index d871f64..e1ec203 100644 --- a/src/adapters/interfaces/IPolygon.sol +++ b/src/adapters/interfaces/IPolygon.sol @@ -2,6 +2,8 @@ // SPDX-License-Identifier: MIT +// solhint-disable func-name-mixedcase + pragma solidity >=0.8.19; struct DelegatorUnbond { @@ -12,6 +14,7 @@ struct DelegatorUnbond { interface IMaticStakeManager { function getValidatorId(address user) external view returns (uint256); function getValidatorContract(uint256 validatorId) external view returns (address); + function epoch() external view returns (uint256); } interface IValidatorShares { diff --git a/src/tenderizer/Tenderizer.sol b/src/tenderizer/Tenderizer.sol index 688b065..2163ea6 100644 --- a/src/tenderizer/Tenderizer.sol +++ b/src/tenderizer/Tenderizer.sol @@ -41,6 +41,7 @@ contract Tenderizer is TenderizerImmutableArgs, TenderizerEvents, TToken, Multic uint256 private constant MAX_FEE = 0.005e6; // 0.5% uint256 private constant FEE_BASE = 1e6; + // solhint-disable-next-line no-empty-blocks constructor(address _registry, address _unlocks) TenderizerImmutableArgs(_registry, _unlocks) { } // @inheritdoc TToken @@ -174,10 +175,6 @@ contract Tenderizer is TenderizerImmutableArgs, TenderizerEvents, TToken, Multic return string.concat(ERC20(asset()).symbol(), "-", addressToString(validator())); } - function _adapter() internal view returns (Adapter) { - return Adapter(_registry().adapter(asset())); - } - function previewDeposit(uint256 assets) external view returns (uint256) { uint256 out = abi.decode(_staticcall(address(this), abi.encodeCall(this._previewDeposit, (assets))), (uint256)); Storage storage $ = _loadStorage(); @@ -201,31 +198,31 @@ contract Tenderizer is TenderizerImmutableArgs, TenderizerEvents, TToken, Multic // using a `staticcall` to `this`. // This is a hacky workaround while better solidity features are being developed. function _previewDeposit(uint256 assets) public returns (uint256) { - return abi.decode(_adapter()._delegatecall(abi.encodeCall(_adapter().previewDeposit, (assets))), (uint256)); + return abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().previewDeposit, (assets))), (uint256)); } function _previewWithdraw(uint256 unlockID) public returns (uint256) { - return abi.decode(_adapter()._delegatecall(abi.encodeCall(_adapter().previewWithdraw, (unlockID))), (uint256)); + return abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().previewWithdraw, (unlockID))), (uint256)); } function _unlockMaturity(uint256 unlockID) public returns (uint256) { - return abi.decode(_adapter()._delegatecall(abi.encodeCall(_adapter().unlockMaturity, (unlockID))), (uint256)); + return abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().unlockMaturity, (unlockID))), (uint256)); } // =============================================================================================================== function _rebase(address validator, uint256 currentStake) internal returns (uint256 newStake) { - newStake = abi.decode(_adapter()._delegatecall(abi.encodeCall(_adapter().rebase, (validator, currentStake))), (uint256)); + newStake = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().rebase, (validator, currentStake))), (uint256)); } function _stake(address validator, uint256 amount) internal { - _adapter()._delegatecall(abi.encodeCall(_adapter().stake, (validator, amount))); + adapter()._delegatecall(abi.encodeCall(adapter().stake, (validator, amount))); } function _unstake(address validator, uint256 amount) internal returns (uint256 unlockID) { - unlockID = abi.decode(_adapter()._delegatecall(abi.encodeCall(_adapter().unstake, (validator, amount))), (uint256)); + unlockID = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().unstake, (validator, amount))), (uint256)); } function _withdraw(address validator, uint256 unlockID) internal returns (uint256 withdrawAmount) { - withdrawAmount = abi.decode(_adapter()._delegatecall(abi.encodeCall(_adapter().withdraw, (validator, unlockID))), (uint256)); + withdrawAmount = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().withdraw, (validator, unlockID))), (uint256)); } } diff --git a/src/tenderizer/TenderizerBase.sol b/src/tenderizer/TenderizerBase.sol index bc0aea4..b3c4dcb 100644 --- a/src/tenderizer/TenderizerBase.sol +++ b/src/tenderizer/TenderizerBase.sol @@ -14,6 +14,7 @@ pragma solidity >=0.8.19; import { Clone } from "clones/Clone.sol"; import { Unlocks } from "core/unlocks/Unlocks.sol"; import { Registry } from "core/registry/Registry.sol"; +import { Adapter } from "core/adapters/Adapter.sol"; /// @title TenderizerImmutableArgs /// @notice Immutable arguments for Tenderizer @@ -46,6 +47,10 @@ abstract contract TenderizerImmutableArgs is Clone { return _getArgAddress(20); // start: 20 end: 39 } + function adapter() public view returns (Adapter) { + return Adapter(_registry().adapter(asset())); + } + function _registry() internal view returns (Registry) { return Registry(registry); } diff --git a/src/unlocks/Unlocks.sol b/src/unlocks/Unlocks.sol index 2fe5dae..c2fa9ab 100644 --- a/src/unlocks/Unlocks.sol +++ b/src/unlocks/Unlocks.sol @@ -12,6 +12,7 @@ import { ERC721 } from "solmate/tokens/ERC721.sol"; import { ERC20 } from "solmate/tokens/ERC20.sol"; +import { Adapter } from "core/adapters/Adapter.sol"; import { Tenderizer } from "core/tenderizer/Tenderizer.sol"; import { Registry } from "core/registry/Registry.sol"; import { Renderer } from "core/unlocks/Renderer.sol"; @@ -27,6 +28,7 @@ pragma solidity >=0.8.19; struct Metadata { uint256 amount; uint256 maturity; + uint256 progress; uint256 unlockId; string symbol; string name; @@ -104,9 +106,14 @@ contract Unlocks is ERC721 { (address tenderizer, uint256 unlockId) = _decodeTokenId(tokenId); address asset = Tenderizer(tenderizer).asset(); + Adapter adapter = Tenderizer(tenderizer).adapter(); + uint256 maturity = Tenderizer(tenderizer).unlockMaturity(unlockId); + uint256 currentTime = adapter.currentTime(); + return Metadata({ amount: Tenderizer(tenderizer).previewWithdraw(unlockId), - maturity: Tenderizer(tenderizer).unlockMaturity(unlockId), + maturity: maturity, + progress: maturity > currentTime ? 100 - (maturity - currentTime) * 100 / adapter.unlockTime() : 100, unlockId: unlockId, symbol: ERC20(asset).symbol(), name: ERC20(asset).name(), diff --git a/src/utils/TWAP.sol b/src/utils/TWAP.sol index 3422efd..560bb66 100644 --- a/src/utils/TWAP.sol +++ b/src/utils/TWAP.sol @@ -11,10 +11,10 @@ pragma solidity >=0.8.19; -import "@uniswap/v3-core/interfaces/IUniswapV3Pool.sol"; -import "@uniswap/v3-core/libraries/TickMath.sol"; -import "@uniswap/v3-core/libraries/FixedPoint96.sol"; -import "@uniswap/v3-core/libraries/FullMath.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/interfaces/IUniswapV3Pool.sol"; +import { TickMath } from "@uniswap/v3-core/libraries/TickMath.sol"; +import { FixedPoint96 } from "@uniswap/v3-core/libraries/FixedPoint96.sol"; +import { FullMath } from "@uniswap/v3-core/libraries/FullMath.sol"; library TWAP { function getSqrtTwapX96(address uniswapV3Pool, uint32 twapInterval) internal view returns (uint160 sqrtPriceX96) { diff --git a/test/adapters/PolygonAdapter.t.sol b/test/adapters/PolygonAdapter.t.sol index b066e9c..5d2b8a5 100644 --- a/test/adapters/PolygonAdapter.t.sol +++ b/test/adapters/PolygonAdapter.t.sol @@ -9,6 +9,9 @@ // // Copyright (c) Tenderize Labs Ltd +// solhint-disable state-visibility +// solhint-disable func-name-mixedcase + pragma solidity >=0.8.19; import { Test, stdError } from "forge-std/Test.sol"; diff --git a/test/helpers/StakingXYZ.sol b/test/helpers/StakingXYZ.sol index bcb9416..c605f3e 100644 --- a/test/helpers/StakingXYZ.sol +++ b/test/helpers/StakingXYZ.sol @@ -15,14 +15,14 @@ import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol"; pragma solidity >=0.8.19; contract StakingXYZ { - address immutable token; + address private immutable token; uint256 public nextRewardTimeStamp; uint256 public immutable APR; uint256 public constant APR_PRECISION = 1e6; uint256 public constant SECONDS_IN_A_YEAR = 31_536_000; - uint256 immutable unlockTime; + uint256 public immutable unlockTime; struct Unlock { uint256 amount; diff --git a/test/helpers/XYZAdapter.sol b/test/helpers/XYZAdapter.sol index 6a774fe..0b610b1 100644 --- a/test/helpers/XYZAdapter.sol +++ b/test/helpers/XYZAdapter.sol @@ -19,8 +19,8 @@ import { StakingXYZ } from "./StakingXYZ.sol"; pragma solidity >=0.8.19; contract XYZAdapter is Adapter { - address immutable STAKINGXYZ; - address immutable XYZ_TOKEN; + address private immutable STAKINGXYZ; + address private immutable XYZ_TOKEN; constructor(address _stakingXYZ, address _xyz) { STAKINGXYZ = _stakingXYZ; @@ -43,6 +43,14 @@ contract XYZAdapter is Adapter { (, maturity) = StakingXYZ(STAKINGXYZ).unlocks(address(this), unlockID); } + function unlockTime() external view returns (uint256) { + return StakingXYZ(STAKINGXYZ).unlockTime(); + } + + function currentTime() external view returns (uint256) { + return block.timestamp; + } + function stake(address, uint256 amount) external { ERC20(XYZ_TOKEN).approve(STAKINGXYZ, amount); StakingXYZ(STAKINGXYZ).stake(amount); diff --git a/test/tenderizer/Tenderizer.harness.sol b/test/tenderizer/Tenderizer.harness.sol index 8eae76b..41061fe 100644 --- a/test/tenderizer/Tenderizer.harness.sol +++ b/test/tenderizer/Tenderizer.harness.sol @@ -16,14 +16,11 @@ import { Unlocks } from "core/unlocks/Unlocks.sol"; import { Registry } from "core/registry/Registry.sol"; // solhint-disable func-name-mixedcase +// solhint-disable no-empty-blocks contract TenderizerHarness is Tenderizer { constructor(address _registry, address _unlocks) Tenderizer(_registry, _unlocks) { } - function exposed_adapter() public view returns (Adapter) { - return _adapter(); - } - function exposed_registry() public view returns (Registry) { return _registry(); } diff --git a/test/tenderizer/Tenderizer.t.sol b/test/tenderizer/Tenderizer.t.sol index bf6a436..5001232 100644 --- a/test/tenderizer/Tenderizer.t.sol +++ b/test/tenderizer/Tenderizer.t.sol @@ -79,7 +79,7 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { assertEq(address(tenderizer.validator()), validator, "invalid validator"); assertEq(address(tenderizer.exposed_registry()), registry, "invalid registry"); assertEq(address(tenderizer.exposed_unlocks()), unlocks, "invalid unlocks"); - assertEq(address(tenderizer.exposed_adapter()), adapter, "invalid adapter"); + assertEq(address(tenderizer.adapter()), adapter, "invalid adapter"); } function test_PreviewDeposit() public { diff --git a/test/unlocks/Renderer.t.sol b/test/unlocks/Renderer.t.sol index e5de115..532badd 100644 --- a/test/unlocks/Renderer.t.sol +++ b/test/unlocks/Renderer.t.sol @@ -59,7 +59,7 @@ contract RendererTest is Test { address private validator = vm.addr(4); uint256 private id = 1; Metadata private metadata = - Metadata({ amount: 100, maturity: 1000, unlockId: id, symbol: "GRT", name: "Graph", validator: validator }); + Metadata({ amount: 100, maturity: 1000, progress: 100, unlockId: id, symbol: "GRT", name: "Graph", validator: validator }); RendererV1 private rendererV1; bytes32 internal constant IMPL_SLOT = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); diff --git a/test/unlocks/Unlocks.t.sol b/test/unlocks/Unlocks.t.sol index ec77d4d..5b30311 100644 --- a/test/unlocks/Unlocks.t.sol +++ b/test/unlocks/Unlocks.t.sol @@ -14,6 +14,7 @@ pragma solidity >=0.8.19; import { Test, stdError } from "forge-std/Test.sol"; import { IERC20Metadata } from "core/interfaces/IERC20.sol"; +import { Adapter } from "core/adapters/Adapter.sol"; import { Renderer } from "core/unlocks/Renderer.sol"; import { Registry } from "core/registry/Registry.sol"; import { Tenderizer } from "core/tenderizer/Tenderizer.sol"; @@ -29,9 +30,11 @@ contract UnlockTest is Test { address private renderer = vm.addr(4); address private impostor = vm.addr(5); address private validator = vm.addr(6); + address private adapter = vm.addr(7); function setUp() public { unlocks = new Unlocks(registry, renderer); + vm.etch(adapter, bytes("code")); } function test_Metadata() public { @@ -142,8 +145,12 @@ contract UnlockTest is Test { vm.mockCall(registry, abi.encodeCall(Registry.isTenderizer, (tenderizer)), abi.encode(true)); uint256 tokenId = unlocks.createUnlock(msg.sender, lockId); + vm.mockCall(tenderizer, abi.encodeCall(TenderizerImmutableArgs.adapter, ()), abi.encode((adapter))); + vm.mockCall(adapter, abi.encodeCall(Adapter.currentTime, ()), abi.encode((block.number + 50))); + vm.mockCall(adapter, abi.encodeCall(Adapter.unlockTime, ()), abi.encode((100))); + vm.mockCall(tenderizer, abi.encodeCall(Tenderizer.previewWithdraw, (lockId)), abi.encode((1 ether))); - vm.mockCall(tenderizer, abi.encodeCall(Tenderizer.unlockMaturity, (lockId)), abi.encode((block.timestamp))); + vm.mockCall(tenderizer, abi.encodeCall(Tenderizer.unlockMaturity, (lockId)), abi.encode((block.number + 100))); vm.mockCall(tenderizer, abi.encodeCall(TenderizerImmutableArgs.validator, ()), abi.encode((validator))); vm.mockCall(tenderizer, abi.encodeCall(TenderizerImmutableArgs.asset, ()), abi.encode((asset))); vm.mockCall(asset, abi.encodeCall(IERC20Metadata.symbol, ()), abi.encode(("TEST"))); @@ -154,7 +161,8 @@ contract UnlockTest is Test { assertEq(d.unlockId, lockId); assertEq(d.amount, 1 ether); - assertEq(d.maturity, block.timestamp); + assertEq(d.maturity, block.number + 100); + assertEq(d.progress, 50); assertEq(d.symbol, "TEST"); assertEq(d.name, "Test Token"); assertEq(d.validator, validator);