diff --git a/staker/consts.gno b/staker/consts.gno index 77e7b9f5..40252a6e 100644 --- a/staker/consts.gno +++ b/staker/consts.gno @@ -10,6 +10,8 @@ const ( Q96 bigint = 79228162514264337593543950336 // 2 ** 96 Q128 bigint = 340282366920938463463374607431768211456 // 2 ** 128 - TIMESTAMP_1DAY int64 = 86400 - TIMESTAMP_30DAYS int64 = 2592000 + TIMESTAMP_1DAY uint64 = 86400 + TIMESTAMP_90DAYS uint64 = 7776000 + TIMESTAMP_180DAYS uint64 = 15552000 + TIMESTAMP_360DAYS uint64 = 31104000 ) diff --git a/staker/reward_math.gno b/staker/reward_math.gno index 89fd1d23..53069c4f 100644 --- a/staker/reward_math.gno +++ b/staker/reward_math.gno @@ -60,13 +60,13 @@ func rewardMathComputeExternalRewardAmount(tokenId uint64, deposit Deposit, ince monthlyReward := uint64(0) switch { - case incentiveDuration == TIMESTAMP_30DAYS: - monthlyReward = incentive.rewardAmount - case incentiveDuration > TIMESTAMP_30DAYS: + case incentiveDuration == TIMESTAMP_90DAYS: + monthlyReward = incentive.rewardAmount / 3 + case incentiveDuration > TIMESTAMP_90DAYS: // 1 second reward == total reward amount / reward duration - monthlyReward = incentive.rewardAmount / uint64(incentiveDuration) * uint64(TIMESTAMP_30DAYS) + monthlyReward = incentive.rewardAmount / uint64(incentiveDuration) default: - panic(ufmt.Sprintf("[STAKER] reward_math.gno || incentiveDuration(%d) at least 30 days", incentiveDuration)) + panic(ufmt.Sprintf("[STAKER] reward_math.gno || incentiveDuration(%d) should be at least 90 days", incentiveDuration)) } // calculate reward amount per block diff --git a/staker/staker.gno b/staker/staker.gno index b92228d3..5a794923 100644 --- a/staker/staker.gno +++ b/staker/staker.gno @@ -6,8 +6,7 @@ import ( "gno.land/p/demo/ufmt" gnft "gno.land/r/gnft" // GNFT, Gnoswap NFT - gns "gno.land/r/gns" // GNS, INTERNAL Reward Token - obl "gno.land/r/obl" // OBL, EXTERNAL Reward Token + gns "gno.land/r/gns" // INTERAL REWARD FIXED s "gno.land/r/position" ) @@ -27,7 +26,7 @@ var ( func init() { // init pool tiers // tier 1 - poolTiers["BAR/FOO_500"] = 1 // DEV + poolTiers["gno.land/r/bar:gno.land/r/foo:500"] = 1 // DEV // tier 2 poolTiers["GNS/USDT_500"] = 2 @@ -41,14 +40,19 @@ func init() { func CreateExternalIncentive( targetPoolPath string, - // rewardToken *grc20.AdminToken, // FIXED TO OBL for now - rewardToken string, + rewardToken string, // token path should be registered rewardAmount uint64, startTimestamp int64, endTimestamp int64, ) { require(GetTimestamp() <= startTimestamp, ufmt.Sprintf("[STAKER] staker.gno__CreateExternalIncentive() || startTimestamp must be in the future__GetTimestamp(%d) <= startTimestamp(%d)", GetTimestamp(), startTimestamp)) - require(endTimestamp-startTimestamp >= TIMESTAMP_30DAYS, ufmt.Sprintf("[STAKER] staker.gno__CreateExternalIncentive() || endTimestamp must be at least 30 days after startTimestamp__endTimestamp-startTimestamp(%d) >= TIMESTAMP_30DAYS(%d)", endTimestamp-startTimestamp, TIMESTAMP_30DAYS)) + + externalDuration := uint64(endTimestamp - startTimestamp) + if !(externalDuration == TIMESTAMP_90DAYS || externalDuration == TIMESTAMP_180DAYS || externalDuration == TIMESTAMP_360DAYS) { + println("externalDuration:", externalDuration) + println("TIMESTAMP_90DAYS:", TIMESTAMP_90DAYS) + panic(ufmt.Sprintf("[STAKER] staker.gno__CreateExternalIncentive() || externalDuration(%d) must be 90, 180, 360 days)", externalDuration)) + } incentiveId := incentiveIdCompute(targetPoolPath, rewardToken) @@ -59,15 +63,14 @@ func CreateExternalIncentive( } } - from := a2u(GetOrigCaller()) - fromBalanceBefore := obl.BalanceOf(from) - require(fromBalanceBefore >= rewardAmount, ufmt.Sprintf("[STAKER] staker.gno__CreateExternalIncentive() || not enough OBL(%s) to create incentive(%s)", fromBalanceBefore, rewardAmount)) + fromBalanceBefore := balanceOfByRegisterCall(rewardToken, GetOrigCaller()) + require(fromBalanceBefore >= rewardAmount, ufmt.Sprintf("[STAKER] staker.gno__CreateExternalIncentive() || not enough rewardAmount(%d) to create incentive(%d)", fromBalanceBefore, rewardAmount)) + + poolRewardBalanceBefore := balanceOfByRegisterCall(rewardToken, GetOrigPkgAddr()) - to := a2u(GetOrigPkgAddr()) // staker contract - poolRewardBalanceBefore := obl.BalanceOf(to) + transferFromByRegisterCall(rewardToken, GetOrigCaller(), GetOrigPkgAddr(), rewardAmount) - obl.TransferFrom(from, to, rewardAmount) - poolRewardBalanceAfter := obl.BalanceOf(to) + poolRewardBalanceAfter := balanceOfByRegisterCall(rewardToken, GetOrigPkgAddr()) require(poolRewardBalanceAfter-poolRewardBalanceBefore == rewardAmount, ufmt.Sprintf("[STAKER] staker.gno__CreateExternalIncentive() || pool reward balance not updated correctly")) incentives[incentiveId] = Incentive{ @@ -87,20 +90,20 @@ func StakeToken( ) { // check whether tokenId already staked or not _, exist := deposits[tokenId] - require(!exist, ufmt.Sprintf("[STAKER] staker.gno__StakeToken() || tokenId(%s) already staked", tokenId)) + require(!exist, ufmt.Sprintf("[STAKER] staker.gno__StakeToken() || tokenId(%d) already staked", tokenId)) // check tokenId owner require( gnft.OwnerOf(tid(tokenId)) == GetOrigCaller(), ufmt.Sprintf( - "[STAKER] staker.gno__StakeToken() || only owner can stake their token__gnft.OwnerOf(tid(tokenId(%s)))(%s) == GetOrigCaller()(%s)", + "[STAKER] staker.gno__StakeToken() || only owner can stake their token__gnft.OwnerOf(tid(tokenId(%d)))(%s) == GetOrigCaller()(%s)", tokenId, gnft.OwnerOf(tid(tokenId)), GetOrigCaller(), ), ) // check tokenId has liquidity or not liquidity := s.PositionGetPositionLiquidity(tokenId) - require(liquidity > 0, ufmt.Sprintf("[STAKER] staker.gno__StakeToken() || tokenId(%s) has no liquidity", tokenId)) + require(liquidity > 0, ufmt.Sprintf("[STAKER] staker.gno__StakeToken() || tokenId(%d) has no liquidity", tokenId)) // check pool path from tokenid poolKey := s.PositionGetPositionPoolKey(tokenId) @@ -120,10 +123,10 @@ func UnstakeToken( tokenId uint64, // GNFT TokenID ) { deposit, exist := deposits[tokenId] - require(exist, ufmt.Sprintf("[STAKER] staker.gno__UnstakeToken() || tokenId(%s) not staked", tokenId)) + require(exist, ufmt.Sprintf("[STAKER] staker.gno__UnstakeToken() || tokenId(%d) not staked", tokenId)) // address who executed StakeToken() can call UnstakeToken() - require(PrevRealmAddr() == deposit.owner, ufmt.Sprintf("[STAKER] staker.gno__UnstakeToken() || only owner(%s) can unstake their token(%s), PrevRealmAddr()(%s)", deposit.owner, tokenId, PrevRealmAddr())) + require(PrevRealmAddr() == deposit.owner, ufmt.Sprintf("[STAKER] staker.gno__UnstakeToken() || only owner(%s) can unstake their token(%d), PrevRealmAddr()(%s)", deposit.owner, tokenId, PrevRealmAddr())) // poolPath to unstake lp token poolPath := s.PositionGetPositionPoolKey(tokenId) @@ -131,11 +134,10 @@ func UnstakeToken( // get all external reward list for this pool for _, incentiveId := range poolIncentives[poolPath] { incentive := incentives[incentiveId] - externalReward := rewardMathComputeExternalRewardAmount(tokenId, deposit, incentive) // OBL reward + externalReward := rewardMathComputeExternalRewardAmount(tokenId, deposit, incentive) // external reward - // r3v4_xxx: get external token then call transfer // r3v4_xxx: handle native coin(gnot) reward - obl.Transfer(a2u(deposit.owner), uint64(externalReward)) + transferByRegisterCall(incentive.rewardToken, deposit.owner, uint64(externalReward)) incentive.rewardAmount -= externalReward incentives[incentiveId] = incentive @@ -157,18 +159,18 @@ func UnstakeToken( func EndExternalIncentive(targetPoolPath, rewardToken string) { incentiveId := incentiveIdCompute(targetPoolPath, rewardToken) incentive, exist := incentives[incentiveId] - require(exist, ufmt.Sprintf("[STAKER] staker.gno__EndIncentive() || cannot end non existent incentive(%s)", incentiveId)) - require(GetTimestamp() >= incentive.endTimestamp, ufmt.Sprintf("[STAKER] staker.gno__EndIncentive() || cannot end incentive before endTimestamp(%s), current(%s)", incentive.endTimestamp, GetTimestamp())) + require(exist, ufmt.Sprintf("[STAKER] staker.gno__EndExternalIncentive() || cannot end non existent incentive(%s)", incentiveId)) + require(GetTimestamp() >= incentive.endTimestamp, ufmt.Sprintf("[STAKER] staker.gno__EndExternalIncentive() || cannot end incentive before endTimestamp(%d), current(%d)", incentive.endTimestamp, GetTimestamp())) // r3v4_xxx: who can end incentive ?? - // require(incentive.refundee == std.GetOrigCaller(), "[STAKER] staker.gno__EndIncentive() || only refundee can end incentive") + // require(incentive.refundee == std.GetOrigCaller(), "[STAKER] staker.gno__EndExternalIncentive() || only refundee can end incentive") refund := incentive.rewardAmount - poolOBL := obl.BalanceOf(a2u(GetOrigPkgAddr())) - require(poolOBL >= refund, ufmt.Sprintf("[STAKER] staker.gno__EndIncentive() || not enough OBL(%s) to refund(%s)", poolOBL, refund)) + poolExternalReward := balanceOfByRegisterCall(incentive.rewardToken, GetOrigPkgAddr()) + require(poolExternalReward >= refund, ufmt.Sprintf("[STAKER] staker.gno__EndExternalIncentive() || not enough poolExternalReward(%d) to refund(%d)", poolExternalReward, refund)) - obl.Transfer(a2u(incentive.refundee), uint64(refund)) + transferByRegisterCall(incentive.rewardToken, incentive.refundee, uint64(refund)) delete(incentives, incentiveId) for i, v := range poolIncentives[targetPoolPath] { @@ -180,7 +182,7 @@ func EndExternalIncentive(targetPoolPath, rewardToken string) { func transferDeposit(tokenId uint64, to std.Address) { owner := gnft.OwnerOf(tid(tokenId)) - require(owner == GetOrigCaller(), ufmt.Sprintf("[STAKER] staker.gno__transferDeposit() || only owner(%s) can transfer tokenId(%s), GetOrigCaller()(%s)", owner, tokenId, PrevRealmAddr())) + require(owner == GetOrigCaller(), ufmt.Sprintf("[STAKER] staker.gno__transferDeposit() || only owner(%s) can transfer tokenId(%d), GetOrigCaller()(%s)", owner, tokenId, PrevRealmAddr())) deposits[tokenId].owner = owner diff --git a/staker/staker_register.gno b/staker/staker_register.gno new file mode 100644 index 00000000..e7cb46a9 --- /dev/null +++ b/staker/staker_register.gno @@ -0,0 +1,121 @@ +package staker + +import ( + "std" + + "gno.land/r/demo/users" +) + +const APPROVED_CALLER = "g12l9splsyngcgefrwa52x5a7scc29e9v086m6p4" // gsa + +var registered = []GRC20Pair{} + +type GRC20Interface interface { + Transfer() func(to users.AddressOrName, amount uint64) + TransferFrom() func(from, to users.AddressOrName, amount uint64) + BalanceOf() func(owner users.AddressOrName) uint64 + Approve() func(spender users.AddressOrName, amount uint64) +} + +type GRC20Pair struct { + pkgPath string + igrc20 GRC20Interface +} + +func findGRC20(pkgPath string) (int, bool) { + for i, pair := range registered { + if pair.pkgPath == pkgPath { + return i, true + } + } + + return -1, false +} + +func appendGRC20Interface(pkgPath string, igrc20 GRC20Interface) { + registered = append(registered, GRC20Pair{pkgPath: pkgPath, igrc20: igrc20}) +} + +func removeGRC20Interface(pkgPath string) { + i, found := findGRC20(pkgPath) + if !found { + return + } + + registered = append(registered[:i], registered[i+1:]...) +} + +func RegisterGRC20Interface(pkgPath string, igrc20 GRC20Interface) { + // only admin can register + // r3v4_xxx: below logic can't be used in test case + // r3v4_xxx: however must be used in production + + // caller := std.GetOrigCaller() + // if caller != APPROVED_CALLER { + // panic("unauthorized address to register") + // } + + _, found := findGRC20(pkgPath) + if !found { + appendGRC20Interface(pkgPath, igrc20) + } +} + +func UnregisterGRC20Interface(pkgPath string) { + // do not allow realm to unregister + std.AssertOriginCall() + + // only admin can unregister + caller := std.GetOrigCaller() + if caller != APPROVED_CALLER { + panic("unauthorized address to unregister") + } + + _, found := findGRC20(pkgPath) + if found { + removeGRC20Interface(pkgPath) + } +} + +func transferByRegisterCall(pkgPath string, to std.Address, amount uint64) bool { + i, found := findGRC20(pkgPath) + if !found { + return false + } + + registered[i].igrc20.Transfer()(users.AddressOrName(to), amount) + + return true +} + +func transferFromByRegisterCall(pkgPath string, from, to std.Address, amount uint64) bool { + i, found := findGRC20(pkgPath) + if !found { + return false + } + + registered[i].igrc20.TransferFrom()(users.AddressOrName(from), users.AddressOrName(to), amount) + + return true +} + +func balanceOfByRegisterCall(pkgPath string, owner std.Address) uint64 { + i, found := findGRC20(pkgPath) + if !found { + return 0 + } + + balance := registered[i].igrc20.BalanceOf()(users.AddressOrName(owner)) + return balance +} + +func approveByRegisterCall(pkgPath string, spender std.Address, amount uint64) bool { + i, found := findGRC20(pkgPath) + if !found { + return false + } + + registered[i].igrc20.Approve()(users.AddressOrName(spender), amount) + + return true +} diff --git a/staker/staker_test.gno b/staker/staker_test.gno index 71a95847..dc1970a2 100644 --- a/staker/staker_test.gno +++ b/staker/staker_test.gno @@ -8,11 +8,13 @@ import ( "gno.land/p/demo/testutils" + g "gno.land/r/gov" p "gno.land/r/pool" pos "gno.land/r/position" gnft "gno.land/r/gnft" // GNFT, Gnoswap NFT gns "gno.land/r/gns" // GNS, Gnoswap Share + obl "gno.land/r/obl" _ "gno.land/r/grc20_wrapper" ) @@ -113,15 +115,15 @@ func TestCreateExternalIncentive(t *testing.T) { CreateExternalIncentive( "gno.land/r/bar:gno.land/r/foo:500", // targetPoolPath - "OBL", // rewardToken + "gno.land/r/obl", // rewardToken 10_000_000_000, // rewardAmount GetTimestamp(), // startTimestamp - GetTimestamp()+TIMESTAMP_30DAYS+TIMESTAMP_30DAYS, // endTimestamp + GetTimestamp()+TIMESTAMP_90DAYS, // endTimestamp ) std.TestSkipHeights(5) shouldPanic(t, func() { - CreateExternalIncentive("gno.land/r/bar:gno.land/r/foo:500", "OBL", 10_000_000_000, GetTimestamp(), GetTimestamp()+TIMESTAMP_30DAYS+TIMESTAMP_30DAYS) + CreateExternalIncentive("gno.land/r/bar:gno.land/r/foo:500", "OBL", 10_000_000_000, GetTimestamp(), GetTimestamp()+TIMESTAMP_90DAYS) }) } @@ -152,10 +154,10 @@ func TestApiGetRewardsByAddress(t *testing.T) { jsonStr := gjson.Parse(gra) shouldEQ(t, jsonStr.Get("response.data.0.type").String(), "Internal") shouldEQ(t, jsonStr.Get("response.data.0.token").String(), "GNS") - shouldEQ(t, jsonStr.Get("response.data.0.reward").Int(), 126) + shouldEQ(t, jsonStr.Get("response.data.0.reward").Int(), 252) shouldEQ(t, jsonStr.Get("response.data.1.type").String(), "External") - shouldEQ(t, jsonStr.Get("response.data.1.token").String(), "OBL") - shouldEQ(t, jsonStr.Get("response.data.1.reward").Int(), 486) + shouldEQ(t, jsonStr.Get("response.data.1.token").String(), "gno.land/r/obl") + shouldEQ(t, jsonStr.Get("response.data.1.reward").Int(), 324) } { @@ -164,10 +166,10 @@ func TestApiGetRewardsByAddress(t *testing.T) { jsonStr := gjson.Parse(gra) shouldEQ(t, jsonStr.Get("response.data.0.type").String(), "Internal") shouldEQ(t, jsonStr.Get("response.data.0.token").String(), "GNS") - shouldEQ(t, jsonStr.Get("response.data.0.reward").Int(), 698) + shouldEQ(t, jsonStr.Get("response.data.0.reward").Int(), 1397) shouldEQ(t, jsonStr.Get("response.data.1.type").String(), "External") - shouldEQ(t, jsonStr.Get("response.data.1.token").String(), "OBL") - shouldEQ(t, jsonStr.Get("response.data.1.reward").Int(), 2696) + shouldEQ(t, jsonStr.Get("response.data.1.token").String(), "gno.land/r/obl") + shouldEQ(t, jsonStr.Get("response.data.1.reward").Int(), 1797) } } @@ -177,11 +179,11 @@ func TestUnstakeToken(t *testing.T) { UnstakeToken(1) // GNFT tokenId std.TestSkipHeights(1) - shouldEQ(t, gnft.OwnerOf(tid(1)), lp01) // lp01 + shouldEQ(t, gnft.OwnerOf(tid(1)), lp01) // check reward shouldEQ(t, gns.BalanceOf(a2u(lp01)), 252) // internal - shouldEQ(t, obl.BalanceOf(a2u(lp01)), 486) // external + shouldEQ(t, obl.BalanceOf(a2u(lp01)), 324) // external } { @@ -189,22 +191,22 @@ func TestUnstakeToken(t *testing.T) { UnstakeToken(2) // GNFT tokenId std.TestSkipHeights(1) - shouldEQ(t, gnft.OwnerOf(tid(2)), lp02) // lp02 + shouldEQ(t, gnft.OwnerOf(tid(2)), lp02) // check reward shouldEQ(t, gns.BalanceOf(a2u(lp02)), 1650) // internal - shouldEQ(t, obl.BalanceOf(a2u(lp02)), 3182) // external + shouldEQ(t, obl.BalanceOf(a2u(lp02)), 2121) // external } } func TestEndExternalIncentive(t *testing.T) { std.TestSetOrigCaller(lp01) - std.TestSkipHeights(1036800) - EndExternalIncentive("bar_foo_500", "OBL") // use same parameter as CreateExternalIncentive() + std.TestSkipHeights(9999999) + EndExternalIncentive("gno.land/r/bar:gno.land/r/foo:500", "gno.land/r/obl") // use same parameter as CreateExternalIncentive() std.TestSkipHeights(1) shouldEQ(t, len(incentives), 0) - shouldEQ(t, len(poolIncentives["bar_foo_500"]), 0) + shouldEQ(t, len(poolIncentives["gno.land/r/bar:gno.land/r/foo:500"]), 0) } // GOV diff --git a/staker/type.gno b/staker/type.gno index f7b62c95..b53762de 100644 --- a/staker/type.gno +++ b/staker/type.gno @@ -6,7 +6,6 @@ import ( type Incentive struct { targetPoolPath string - // rewardToken std.Address rewardToken string rewardAmount uint64 startTimestamp int64