@@ -13,16 +13,18 @@ import (
1313 "github.com/ethereum/go-ethereum/accounts/abi"
1414 "github.com/ethereum/go-ethereum/common"
1515 "github.com/ethereum/go-ethereum/core/types"
16+ "github.com/ethersphere/go-sw3-abi/sw3abi"
17+
1618 "github.com/ethersphere/bee/v2/pkg/sctx"
1719 "github.com/ethersphere/bee/v2/pkg/transaction"
1820 "github.com/ethersphere/bee/v2/pkg/util/abiutil"
19- "github.com/ethersphere/go-sw3-abi/sw3abi"
2021)
2122
2223var (
2324 MinimumStakeAmount = big .NewInt (100000000000000000 )
2425
25- erc20ABI = abiutil .MustParseABI (sw3abi .ERC20ABIv0_6_9 )
26+ erc20ABI = abiutil .MustParseABI (sw3abi .ERC20ABIv0_6_9 )
27+ priceOracleABI = abiutil .MustParseABI (`[{"inputs":[],"name":"currentPrice","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"}]` )
2628
2729 ErrInsufficientStakeAmount = errors .New ("insufficient stake amount" )
2830 ErrInsufficientFunds = errors .New ("insufficient token balance" )
@@ -37,10 +39,25 @@ var (
3739 migrateStakeDescription = "Migrate stake"
3840)
3941
42+ // MinDepositError is returned when a deposit is below the amount required by the
43+ // staking contract, including the non-decreasing commitment rule.
44+ type MinDepositError struct {
45+ Minimum * big.Int
46+ }
47+
48+ func (e * MinDepositError ) Error () string {
49+ return fmt .Sprintf ("insufficient stake amount: minimum %s" , e .Minimum )
50+ }
51+
52+ func (e * MinDepositError ) Unwrap () error {
53+ return ErrInsufficientStakeAmount
54+ }
55+
4056type Contract interface {
4157 DepositStake (ctx context.Context , stakedAmount * big.Int ) (common.Hash , error )
4258 ChangeStakeOverlay (ctx context.Context , nonce common.Hash ) (common.Hash , error )
4359 GetPotentialStake (ctx context.Context ) (* big.Int , error )
60+ GetMinDeposit (ctx context.Context ) (* big.Int , error )
4461 GetWithdrawableStake (ctx context.Context ) (* big.Int , error )
4562 WithdrawStake (ctx context.Context ) (common.Hash , error )
4663 MigrateStake (ctx context.Context ) (common.Hash , error )
@@ -86,19 +103,13 @@ func New(
86103}
87104
88105func (c * contract ) DepositStake (ctx context.Context , stakedAmount * big.Int ) (common.Hash , error ) {
89- prevStakedAmount , err := c .GetPotentialStake (ctx )
106+ minDeposit , err := c .GetMinDeposit (ctx )
90107 if err != nil {
91108 return common.Hash {}, err
92109 }
93110
94- if len (prevStakedAmount .Bits ()) == 0 {
95- if stakedAmount .Cmp (MinimumStakeAmount ) == - 1 {
96- return common.Hash {}, ErrInsufficientStakeAmount
97- }
98- }
99-
100- if big .NewInt (0 ).Add (prevStakedAmount , stakedAmount ).Cmp (big .NewInt (0 ).Mul (big .NewInt (1 << c .height ), MinimumStakeAmount )) < 0 {
101- return common.Hash {}, fmt .Errorf ("stake amount does not sufficiently cover the additional reserve capacity: %w" , ErrInsufficientStakeAmount )
111+ if stakedAmount .Cmp (minDeposit ) < 0 {
112+ return common.Hash {}, & MinDepositError {Minimum : minDeposit }
102113 }
103114
104115 balance , err := c .getBalance (ctx )
@@ -154,11 +165,59 @@ func (c *contract) UpdateHeight(ctx context.Context) (common.Hash, bool, error)
154165}
155166
156167func (c * contract ) GetPotentialStake (ctx context.Context ) (* big.Int , error ) {
157- stakedAmount , err := c .getPotentialStake (ctx )
168+ _ , potential , err := c .getStake (ctx )
158169 if err != nil {
159170 return nil , fmt .Errorf ("staking contract: failed to get stake: %w" , err )
160171 }
161- return stakedAmount , nil
172+ return potential , nil
173+ }
174+
175+ func (c * contract ) GetMinDeposit (ctx context.Context ) (* big.Int , error ) {
176+ committed , potential , err := c .getStake (ctx )
177+ if err != nil {
178+ return nil , fmt .Errorf ("staking contract: failed to get stake: %w" , err )
179+ }
180+
181+ var price uint32
182+ if committed .Sign () > 0 {
183+ price , err = c .getCurrentPrice (ctx )
184+ if err != nil {
185+ return nil , fmt .Errorf ("staking contract: failed to get oracle price: %w" , err )
186+ }
187+ }
188+
189+ return calculateMinDeposit (potential , committed , price , c .height ), nil
190+ }
191+
192+ // calculateMinDeposit returns the minimum additional deposit in PLUR that
193+ // manageStake will accept. The first deposit must cover 2^height * MIN_STAKE.
194+ // Later deposits must keep committed stake from decreasing after a price
195+ // increase; if that constraint is already satisfied the minimum is 1 PLUR.
196+ func calculateMinDeposit (potential , committed * big.Int , price uint32 , height uint8 ) * big.Int {
197+ minAdd := new (big.Int )
198+
199+ minTotal := new (big.Int ).Lsh (new (big.Int ).Set (MinimumStakeAmount ), uint (height ))
200+ if gap := new (big.Int ).Sub (minTotal , potential ); gap .Sign () > 0 {
201+ minAdd .Set (gap )
202+ }
203+
204+ if price != 0 && committed .Sign () > 0 {
205+ required := new (big.Int ).SetUint64 (uint64 (price ))
206+ required .Lsh (required , uint (height ))
207+ required .Mul (required , committed )
208+ if gap := new (big.Int ).Sub (required , potential ); gap .Cmp (minAdd ) > 0 {
209+ minAdd .Set (gap )
210+ }
211+ }
212+
213+ if minAdd .Sign () == 0 {
214+ if potential .Sign () > 0 {
215+ return big .NewInt (1 )
216+ }
217+ return new (big.Int ).Set (MinimumStakeAmount )
218+ }
219+
220+ return minAdd
162221}
163222
164223func (c * contract ) GetWithdrawableStake (ctx context.Context ) (* big.Int , error ) {
@@ -327,17 +386,17 @@ func (c *contract) sendManageStakeTransaction(ctx context.Context, stakedAmount
327386 return receipt , nil
328387}
329388
330- func (c * contract ) getPotentialStake (ctx context.Context ) (* big.Int , error ) {
389+ func (c * contract ) getStake (ctx context.Context ) (committed , potential * big.Int , err error ) {
331390 callData , err := c .stakingContractABI .Pack ("stakes" , c .owner )
332391 if err != nil {
333- return nil , err
392+ return nil , nil , err
334393 }
335394 result , err := c .transactionService .Call (ctx , & transaction.TxRequest {
336395 To : & c .stakingContractAddress ,
337396 Data : callData ,
338397 })
339398 if err != nil {
340- return nil , fmt .Errorf ("get potential stake: %w" , err )
399+ return nil , nil , fmt .Errorf ("get potential stake: %w" , err )
341400 }
342401
343402 // overlay bytes32,
@@ -346,14 +405,71 @@ func (c *contract) getPotentialStake(ctx context.Context) (*big.Int, error) {
346405 // lastUpdatedBlockNumber uint256,
347406 results , err := c .stakingContractABI .Unpack ("stakes" , result )
348407 if err != nil {
349- return nil , err
408+ return nil , nil , err
350409 }
351410
352411 if len (results ) < 4 {
353- return nil , ErrUnexpectedLength
412+ return nil , nil , ErrUnexpectedLength
413+ }
414+
415+ committed = abi .ConvertType (results [1 ], new (big.Int )).(* big.Int )
416+ potential = abi .ConvertType (results [2 ], new (big.Int )).(* big.Int )
417+ return committed , potential , nil
418+ }
419+
420+ func (c * contract ) getCurrentPrice (ctx context.Context ) (uint32 , error ) {
421+ callData , err := c .stakingContractABI .Pack ("OracleContract" )
422+ if err != nil {
423+ return 0 , err
424+ }
425+
426+ result , err := c .transactionService .Call (ctx , & transaction.TxRequest {
427+ To : & c .stakingContractAddress ,
428+ Data : callData ,
429+ })
430+ if err != nil {
431+ return 0 , fmt .Errorf ("get oracle address: %w" , err )
432+ }
433+
434+ results , err := c .stakingContractABI .Unpack ("OracleContract" , result )
435+ if err != nil {
436+ return 0 , err
437+ }
438+
439+ if len (results ) == 0 {
440+ return 0 , errors .New ("unexpected empty results" )
441+ }
442+
443+ oracleAddr := * abi .ConvertType (results [0 ], new (common.Address )).(* common.Address )
444+
445+ callData , err = priceOracleABI .Pack ("currentPrice" )
446+ if err != nil {
447+ return 0 , err
448+ }
449+
450+ result , err = c .transactionService .Call (ctx , & transaction.TxRequest {
451+ To : & oracleAddr ,
452+ Data : callData ,
453+ })
454+ if err != nil {
455+ return 0 , fmt .Errorf ("get current price: %w" , err )
456+ }
457+
458+ results , err = priceOracleABI .Unpack ("currentPrice" , result )
459+ if err != nil {
460+ return 0 , err
461+ }
462+
463+ if len (results ) == 0 {
464+ return 0 , errors .New ("unexpected empty results" )
465+ }
466+
467+ price , ok := results [0 ].(uint32 )
468+ if ! ok {
469+ return 0 , fmt .Errorf ("unexpected oracle price type %T" , results [0 ])
354470 }
355471
356- return abi . ConvertType ( results [ 2 ], new (big. Int )).( * big. Int ) , nil
472+ return price , nil
357473}
358474
359475func (c * contract ) getWithdrawableStake (ctx context.Context ) (* big.Int , error ) {
0 commit comments