From b204779323a91d602599a667cec76b8df5f5e45c Mon Sep 17 00:00:00 2001 From: Victoria Zotova Date: Mon, 25 Sep 2023 20:49:18 -0400 Subject: [PATCH 1/2] StakingEscrow: function to wrap and topUp stake in threshold staking contract without withdrawing to staker's account --- contracts/contracts/StakingEscrow.sol | 64 ++++++++++- contracts/test/StakingEscrowTestSet.sol | 105 +++++++++++++++++- tests/staking_escrow/conftest.py | 35 +++++- tests/staking_escrow/test_staking_escrow.py | 64 +++++++++++ .../test_staking_escrow_additional.py | 27 ++++- 5 files changed, 277 insertions(+), 18 deletions(-) diff --git a/contracts/contracts/StakingEscrow.sol b/contracts/contracts/StakingEscrow.sol index aa9ec8c5..0a8d738a 100644 --- a/contracts/contracts/StakingEscrow.sol +++ b/contracts/contracts/StakingEscrow.sol @@ -20,6 +20,21 @@ interface WorkLockInterface { } +/** +* @notice VendingMachine interface +*/ +interface IVendingMachine { + + function wrappedToken() external returns (IERC20); + function tToken() external returns (IERC20); + function wrap(uint256 amount) external; + function unwrap(uint256 amount) external; + function conversionToT(uint256 amount) external view returns (uint256 tAmount, uint256 wrappedRemainder); + function conversionFromT(uint256 amount) external view returns (uint256 wrappedAmount, uint256 tRemainder); + +} + + /** * @title StakingEscrowStub * @notice Stub is used to deploy main StakingEscrow after all other contract and make some variables immutable @@ -110,6 +125,13 @@ contract StakingEscrow is Upgradeable, IERC900History { * @param stakingProvider Staking provider address */ event MergeRequested(address indexed staker, address indexed stakingProvider); + + /** + * @notice Signals that NU tokens were wrapped and topped up to the existing T stake + * @param staker Staker address + * @param value Amount wrapped (in NuNits) + */ + event WrappedAndTopedUp(address indexed staker, uint256 value); struct StakerInfo { uint256 value; @@ -145,6 +167,8 @@ contract StakingEscrow is Upgradeable, IERC900History { NuCypherToken public immutable token; WorkLockInterface public immutable workLock; IStaking public immutable tStaking; + IERC20 public immutable tToken; + IVendingMachine public immutable vendingMachine; uint128 private stub1; // former slot for previousPeriodSupply uint128 public currentPeriodSupply; // resulting token supply @@ -168,21 +192,28 @@ contract StakingEscrow is Upgradeable, IERC900History { * @param _token NuCypher token contract * @param _workLock WorkLock contract. Zero address if there is no WorkLock * @param _tStaking T token staking contract + * @param _vendingMachine Nu vending machine */ constructor( NuCypherToken _token, WorkLockInterface _workLock, - IStaking _tStaking + IStaking _tStaking, + IERC20 _tToken, + IVendingMachine _vendingMachine ) { require(_token.totalSupply() > 0 && _tStaking.stakedNu(address(0)) == 0 && - (address(_workLock) == address(0) || _workLock.token() == _token), + (address(_workLock) == address(0) || _workLock.token() == _token) && + _vendingMachine.wrappedToken() == _token && + _vendingMachine.tToken() == _tToken, "Input addresses must be deployed contracts" ); token = _token; workLock = _workLock; tStaking = _tStaking; + tToken = _tToken; + vendingMachine = _vendingMachine; } /** @@ -273,6 +304,7 @@ contract StakingEscrow is Upgradeable, IERC900History { function withdraw(uint256 _value) external onlyStaker { require(_value > 0, "Value must be specified"); StakerInfo storage info = stakerInfo[msg.sender]; + // TODO remove that line after 2 step of upgrading TokenStaking require( _value + tStaking.stakedNu(info.stakingProvider) <= info.value, "Not enough tokens unstaked in T staking contract" @@ -287,6 +319,34 @@ contract StakingEscrow is Upgradeable, IERC900History { emit Withdrawn(msg.sender, _value); } + /** + * @notice Wraps all tokens and top up stake in T staking contract + */ + function wrapAndTopUp() external onlyStaker { + StakerInfo storage info = stakerInfo[msg.sender]; + require(info.stakingProvider != address(0), "There is no stake in T staking contract"); + require( + tStaking.stakedNu(info.stakingProvider) == 0, + "Not all tokens unstaked in T staking contract" + ); + require( + getUnvestedTokens(msg.sender) == 0, + "Not all tokens released during vesting" + ); + + (uint256 tTokenAmount, uint256 remainder) = vendingMachine.conversionToT( + info.value + ); + + uint256 wrappedTokenAmount = info.value - remainder; + token.approve(address(vendingMachine), wrappedTokenAmount); + vendingMachine.wrap(wrappedTokenAmount); + tToken.approve(address(tStaking), tTokenAmount); + tStaking.topUp(info.stakingProvider, uint96(tTokenAmount)); + info.value = remainder; + emit WrappedAndTopedUp(msg.sender, wrappedTokenAmount); + } + /** * @notice Returns amount of not released yet tokens for staker */ diff --git a/contracts/test/StakingEscrowTestSet.sol b/contracts/test/StakingEscrowTestSet.sol index 14ee50c2..06f4357c 100644 --- a/contracts/test/StakingEscrowTestSet.sol +++ b/contracts/test/StakingEscrowTestSet.sol @@ -6,6 +6,18 @@ pragma solidity ^0.8.0; import "../contracts/StakingEscrow.sol"; import "../contracts/NuCypherToken.sol"; import "@threshold/contracts/staking/IStaking.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + + +/** + * @notice Contract for testing the application contract + */ +contract TToken is ERC20("T", "T") { + constructor(uint256 _totalSupplyOfTokens) { + _mint(msg.sender, _totalSupplyOfTokens); + } +} + /** * @notice Enhanced version of StakingEscrow to use in tests @@ -15,12 +27,16 @@ contract EnhancedStakingEscrow is StakingEscrow { constructor( NuCypherToken _token, WorkLockInterface _workLock, - IStaking _tStaking + IStaking _tStaking, + IERC20 _tToken, + IVendingMachine _vendingMachine ) StakingEscrow( _token, _workLock, - _tStaking + _tStaking, + _tToken, + _vendingMachine ) { } @@ -43,12 +59,16 @@ contract StakingEscrowBad is StakingEscrow { constructor( NuCypherToken _token, WorkLockInterface _workLock, - IStaking _tStaking + IStaking _tStaking, + IERC20 _tToken, + IVendingMachine _vendingMachine ) StakingEscrow( _token, _workLock, - _tStaking + _tStaking, + _tToken, + _vendingMachine ) { } @@ -68,12 +88,16 @@ contract StakingEscrowV2Mock is StakingEscrow { constructor( NuCypherToken _token, WorkLockInterface _workLock, - IStaking _tStaking + IStaking _tStaking, + IERC20 _tToken, + IVendingMachine _vendingMachine ) StakingEscrow( _token, _workLock, - _tStaking + _tStaking, + _tToken, + _vendingMachine ) { valueToCheck = 2; @@ -128,6 +152,7 @@ contract WorkLockForStakingEscrowMock { */ contract ThresholdStakingForStakingEscrowMock { + IERC20 public immutable tToken; StakingEscrow public escrow; struct StakingProviderInfo { @@ -137,6 +162,10 @@ contract ThresholdStakingForStakingEscrowMock { mapping(address => StakingProviderInfo) public stakingProviders; + constructor(IERC20 _tToken) { + tToken = _tToken; + } + function setStakingEscrow(StakingEscrow _escrow) external { escrow = _escrow; } @@ -185,4 +214,68 @@ contract ThresholdStakingForStakingEscrowMock { require(_minStaked <= stakingProviders[_stakingProvider].staked); stakingProviders[_stakingProvider].minStaked = _minStaked; } + + function topUp(address _stakingProvider, uint96 _amount) external { + stakingProviders[_stakingProvider].staked += _amount; + tToken.transferFrom(msg.sender, address(this), _amount); + } } + + +contract VendingMachineForStakingEscrowMock { + using SafeERC20 for IERC20; + + uint256 public constant WRAPPED_TOKEN_CONVERSION_PRECISION = 3; + uint256 public constant FLOATING_POINT_DIVISOR = + 10**(18 - WRAPPED_TOKEN_CONVERSION_PRECISION); + + + IERC20 public immutable wrappedToken; + IERC20 public immutable tToken; + + + constructor( + IERC20 _wrappedToken, + IERC20 _tToken + ) { + wrappedToken = _wrappedToken; + tToken = _tToken; + } + + function wrap(uint256 amount) external { + (uint256 tTokenAmount, uint256 remainder) = conversionToT( + amount + ); + amount -= remainder; + + wrappedToken.safeTransferFrom( + msg.sender, + address(this), + amount + ); + tToken.safeTransfer(msg.sender, tTokenAmount); + } + + function unwrap(uint256 amount) external { + (uint256 wrappedTokenAmount, uint256 remainder) = conversionFromT( + amount + ); + amount -= remainder; + + tToken.safeTransferFrom(msg.sender, address(this), amount); + wrappedToken.safeTransfer(msg.sender, wrappedTokenAmount); + } + + function conversionToT(uint256 amount) public view returns (uint256 tAmount, uint256 wrappedRemainder) { + wrappedRemainder = amount % FLOATING_POINT_DIVISOR; + uint256 convertibleAmount = amount - wrappedRemainder; + tAmount = convertibleAmount; + } + + function conversionFromT(uint256 amount) public view returns (uint256 wrappedAmount, uint256 tRemainder) { + tRemainder = amount % FLOATING_POINT_DIVISOR; + uint256 convertibleAmount = amount - tRemainder; + wrappedAmount = convertibleAmount; + } + +} \ No newline at end of file diff --git a/tests/staking_escrow/conftest.py b/tests/staking_escrow/conftest.py index 7c7bcb8b..d31b73f2 100644 --- a/tests/staking_escrow/conftest.py +++ b/tests/staking_escrow/conftest.py @@ -16,7 +16,7 @@ """ import pytest -import ape +from ape import project from web3 import Web3 TOTAL_SUPPLY = Web3.to_wei(1_000_000_000, "ether") # TODO NU(1_000_000_000, 'NU').to_units() @@ -29,6 +29,13 @@ def token(project, accounts): return token +@pytest.fixture() +def t_token(project, accounts): + # Create an ERC20 token + token = accounts[0].deploy(project.TToken, TOTAL_SUPPLY) + return token + + @pytest.fixture() def worklock(project, token, accounts): worklock = accounts[0].deploy(project.WorkLockForStakingEscrowMock, token.address) @@ -36,16 +43,34 @@ def worklock(project, token, accounts): @pytest.fixture() -def threshold_staking(project, accounts): - threshold_staking = accounts[0].deploy(project.ThresholdStakingForStakingEscrowMock) +def threshold_staking(project, t_token, accounts): + threshold_staking = accounts[0].deploy( + project.ThresholdStakingForStakingEscrowMock, t_token.address + ) return threshold_staking +@pytest.fixture() +def vending_machine(token, t_token, accounts): + contract = accounts[0].deploy( + project.VendingMachineForStakingEscrowMock, token.address, t_token.address + ) + t_token.transfer(contract.address, TOTAL_SUPPLY, sender=accounts[0]) + return contract + + @pytest.fixture(params=[False, True]) -def escrow(project, token, worklock, threshold_staking, request, accounts): +def escrow( + project, token, worklock, threshold_staking, vending_machine, t_token, request, accounts +): creator = accounts[0] contract = creator.deploy( - project.EnhancedStakingEscrow, token.address, worklock.address, threshold_staking.address + project.EnhancedStakingEscrow, + token.address, + worklock.address, + threshold_staking.address, + t_token.address, + vending_machine.address, ) if request.param: diff --git a/tests/staking_escrow/test_staking_escrow.py b/tests/staking_escrow/test_staking_escrow.py index 3395e91b..8032ac27 100644 --- a/tests/staking_escrow/test_staking_escrow.py +++ b/tests/staking_escrow/test_staking_escrow.py @@ -23,6 +23,7 @@ VESTING_RELEASE_RATE_SLOT = 10 STAKING_PROVIDER_SLOT = 11 ONE_HOUR = 60 * 60 +TOTAL_SUPPLY = Web3.to_wei(1_000_000_000, "ether") def test_staking_from_worklock(project, accounts, token, worklock, escrow): @@ -432,3 +433,66 @@ def test_combined_vesting(accounts, token, worklock, escrow, chain): chain.pending_timestamp += 20 * 60 assert escrow.getUnvestedTokens(staker) == 0 + + +def test_wrap( + accounts, token, worklock, threshold_staking, escrow, vending_machine, t_token, chain +): + creator, staker, staking_provider, other_staker = accounts[0:4] + + with ape.reverts(): + escrow.wrapAndTopUp(sender=staker) + + # Deposit some tokens + value = Web3.to_wei(15_000, "ether") + token.transfer(worklock.address, 10 * value, sender=creator) + worklock.depositFromWorkLock(staker, value, 0, sender=creator) + + # Can't wrap without requesting merge + with ape.reverts(): + escrow.wrapAndTopUp(sender=staker) + + threshold_staking.requestMerge(staker, staking_provider, sender=creator) + + with ape.reverts(): + escrow.wrapAndTopUp(sender=staker) + + threshold_staking.setStakedNu(staking_provider, 0, sender=creator) + now = chain.pending_timestamp + release_timestamp = now + ONE_HOUR + rate = value // ONE_HOUR + escrow.setupVesting([staker], [release_timestamp], [rate], sender=creator) + + with ape.reverts(): + escrow.wrapAndTopUp(sender=staker) + + chain.pending_timestamp += ONE_HOUR + + tx = escrow.wrapAndTopUp(sender=staker) + assert escrow.getAllTokens(staker) == 0 + assert token.balanceOf(staker) == 0 + assert token.balanceOf(escrow.address) == 0 + assert token.balanceOf(vending_machine.address) == value + assert t_token.balanceOf(threshold_staking.address) == value + assert t_token.balanceOf(vending_machine.address) == TOTAL_SUPPLY - value + + events = escrow.WrappedAndTopedUp.from_receipt(tx) + assert events == [escrow.WrappedAndTopedUp(staker=staker, value=value)] + + # Wrap again but with remainder + other_value = Web3.to_wei(15_000, "ether") + 1 + worklock.depositFromWorkLock(other_staker, other_value, 0, sender=creator) + threshold_staking.requestMerge(other_staker, other_staker, sender=creator) + threshold_staking.setStakedNu(other_staker, 0, sender=creator) + + tx = escrow.wrapAndTopUp(sender=other_staker) + expected_value = value + other_value - 1 + assert escrow.getAllTokens(other_staker) == 1 + assert token.balanceOf(other_staker) == 0 + assert token.balanceOf(escrow.address) == 1 + assert token.balanceOf(vending_machine.address) == expected_value + assert t_token.balanceOf(threshold_staking.address) == expected_value + assert t_token.balanceOf(vending_machine.address) == TOTAL_SUPPLY - expected_value + + events = escrow.WrappedAndTopedUp.from_receipt(tx) + assert events == [escrow.WrappedAndTopedUp(staker=other_staker, value=other_value - 1)] diff --git a/tests/staking_escrow/test_staking_escrow_additional.py b/tests/staking_escrow/test_staking_escrow_additional.py index a853dcf6..e47c6024 100644 --- a/tests/staking_escrow/test_staking_escrow_additional.py +++ b/tests/staking_escrow/test_staking_escrow_additional.py @@ -20,17 +20,24 @@ from web3 import Web3 -def test_upgrading(accounts, token, project): +def test_upgrading(accounts, token, t_token, vending_machine, project): creator = accounts[0] staker = accounts[1] # Initialize contract and staker worklock = creator.deploy(project.WorkLockForStakingEscrowMock, token.address) - threshold_staking = creator.deploy(project.ThresholdStakingForStakingEscrowMock) + threshold_staking = creator.deploy( + project.ThresholdStakingForStakingEscrowMock, t_token.address + ) # Deploy contract contract_library_v1 = creator.deploy( - project.StakingEscrow, token.address, worklock.address, threshold_staking.address + project.StakingEscrow, + token.address, + worklock.address, + threshold_staking.address, + t_token.address, + vending_machine.address, ) dispatcher = creator.deploy(project.Dispatcher, contract_library_v1.address) @@ -43,7 +50,12 @@ def test_upgrading(accounts, token, project): # Deploy second version of the contract contract_library_v2 = creator.deploy( - project.StakingEscrowV2Mock, token.address, worklock.address, threshold_staking.address + project.StakingEscrowV2Mock, + token.address, + worklock.address, + threshold_staking.address, + t_token.address, + vending_machine.address, ) contract = project.StakingEscrowV2Mock.at(dispatcher.address) @@ -78,7 +90,12 @@ def test_upgrading(accounts, token, project): # Can't upgrade to the previous version or to the bad version contract_library_bad = creator.deploy( - project.StakingEscrowBad, token.address, worklock.address, threshold_staking.address + project.StakingEscrowBad, + token.address, + worklock.address, + threshold_staking.address, + t_token.address, + vending_machine.address, ) with ape.reverts(): dispatcher.upgrade(contract_library_v1.address, sender=creator) From e4ef53b7efbdc0adc46c38276543c7790ec0caa5 Mon Sep 17 00:00:00 2001 From: Victoria Date: Tue, 10 Oct 2023 19:49:42 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Derek Pierre --- contracts/contracts/StakingEscrow.sol | 4 ++-- contracts/contracts/testnet/OpenAccessAuthorizer.sol | 1 - tests/staking_escrow/test_staking_escrow.py | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/StakingEscrow.sol b/contracts/contracts/StakingEscrow.sol index 0a8d738a..1aba6eda 100644 --- a/contracts/contracts/StakingEscrow.sol +++ b/contracts/contracts/StakingEscrow.sol @@ -131,7 +131,7 @@ contract StakingEscrow is Upgradeable, IERC900History { * @param staker Staker address * @param value Amount wrapped (in NuNits) */ - event WrappedAndTopedUp(address indexed staker, uint256 value); + event WrappedAndToppedUp(address indexed staker, uint256 value); struct StakerInfo { uint256 value; @@ -344,7 +344,7 @@ contract StakingEscrow is Upgradeable, IERC900History { tToken.approve(address(tStaking), tTokenAmount); tStaking.topUp(info.stakingProvider, uint96(tTokenAmount)); info.value = remainder; - emit WrappedAndTopedUp(msg.sender, wrappedTokenAmount); + emit WrappedAndToppedUp(msg.sender, wrappedTokenAmount); } /** diff --git a/contracts/contracts/testnet/OpenAccessAuthorizer.sol b/contracts/contracts/testnet/OpenAccessAuthorizer.sol index 77e81c6d..b94b71e0 100644 --- a/contracts/contracts/testnet/OpenAccessAuthorizer.sol +++ b/contracts/contracts/testnet/OpenAccessAuthorizer.sol @@ -10,7 +10,6 @@ interface IEncryptionAuthorizer { ) external view returns (bool); } - contract OpenAccessAuthorizer is IEncryptionAuthorizer { function isAuthorized( uint32, diff --git a/tests/staking_escrow/test_staking_escrow.py b/tests/staking_escrow/test_staking_escrow.py index 8032ac27..1166980c 100644 --- a/tests/staking_escrow/test_staking_escrow.py +++ b/tests/staking_escrow/test_staking_escrow.py @@ -476,8 +476,8 @@ def test_wrap( assert t_token.balanceOf(threshold_staking.address) == value assert t_token.balanceOf(vending_machine.address) == TOTAL_SUPPLY - value - events = escrow.WrappedAndTopedUp.from_receipt(tx) - assert events == [escrow.WrappedAndTopedUp(staker=staker, value=value)] + events = escrow.WrappedAndToppedUp.from_receipt(tx) + assert events == [escrow.WrappedAndToppedUp(staker=staker, value=value)] # Wrap again but with remainder other_value = Web3.to_wei(15_000, "ether") + 1 @@ -494,5 +494,5 @@ def test_wrap( assert t_token.balanceOf(threshold_staking.address) == expected_value assert t_token.balanceOf(vending_machine.address) == TOTAL_SUPPLY - expected_value - events = escrow.WrappedAndTopedUp.from_receipt(tx) - assert events == [escrow.WrappedAndTopedUp(staker=other_staker, value=other_value - 1)] + events = escrow.WrappedAndToppedUp.from_receipt(tx) + assert events == [escrow.WrappedAndToppedUp(staker=other_staker, value=other_value - 1)]