Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Preaudit #465

Draft
wants to merge 20 commits into
base: init
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 0 additions & 1 deletion README.md

This file was deleted.

137 changes: 137 additions & 0 deletions contract/p/gnoswap/consts/consts.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package consts

import (
"std"
)

// GNOSWAP SERVICE
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The casing of the comments in this file is inconsistent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const (
ADMIN std.Address = "g17290cwvmrapvp869xfnhhawa8sm9edpufzat7d"
DEV_OPS std.Address = "g1mjvd83nnjee3z2g7683er55me9f09688pd4mj9"
TOKEN_REGISTER std.Address = "g1er355fkjksqpdtwmhf5penwa82p0rhqxkkyhk5"

TOKEN_REGISTER_NAMESPACE string = "gno.land/r/g1er355fkjksqpdtwmhf5penwa82p0rhqxkkyhk5"

BLOCK_GENERATION_INTERVAL int64 = 2 // seconds

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is potentially very dangerous -- block time is a variable protocol trait that you should not hardcode or anticipate in any manner. Imagine a chain halt. If we don't already, we should have protocol-level support in Gno for fetching the block time, and deriving the interval accordingly.

I see you use this in the halving code, so please revise this 🙏

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have a function in place to regulate this time, which will collect the time at which 1 block is created and update it to an average value, which will be managed. Adjusting the time will be modified through transactions with external contracts, and due to the nature of the service, services that need to be provided based on a time value require the time value when a block is created. For example, proposals in governance, projects in launchpad, and external incentives in staker need to be managed when they end. To calculate this only by the time value, there is a problem that when the chain stops, as you said, the time is passing, so the reward should be paid. Considering that the chain can stall, we need to calculate based on block height rather than time, in which case we need to generate a termination point at which block it should end and have a system to distribute it evenly, which requires a value for the creation time of the block, which is initially the Default value.

)

// WRAP & UNWRAP
const (
GNOT string = "gnot"
UGNOT string = "ugnot"
WRAPPED_WUGNOT string = "gno.land/r/demo/wugnot"

// defined in https://github.com/gnolang/gno/blob/81a88a2976ba9f2f9127ebbe7fb7d1e1f7fa4bd4/examples/gno.land/r/demo/wugnot/wugnot.gno#L19
UGNOT_MIN_DEPOSIT_TO_WRAP uint64 = 1000
)

// CONTRACT PATH & ADDRESS
const (
POOL_PATH string = "gno.land/r/gnoswap/v1/pool"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all of your realm paths should be modifiable through some kind of structure, and with proper ownership handling (ex. see the govdao bridge realm in the examples), and not hardcoded

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally agree, but given the current codebase, I prefer redeploying everything except the components that will own assets. Attempting to partially upgrade the codebase, which has many levels of interdependency, seems:

  1. undoable
  2. untrustworthy

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All contracts are dependent on the emission. The minting and distribution of GNS is done by emission, and the addresses that are distributed are determined by emission. As you said, it is correct to modify the whitelist or update of contract addresses to be able to distribute updated versions of contracts individually, not hard-coded.

POOL_ADDR std.Address = "g148tjamj80yyrm309z7rk690an22thd2l3z8ank"

POSITION_PATH string = "gno.land/r/gnoswap/v1/position"
POSITION_ADDR std.Address = "g1q646ctzhvn60v492x8ucvyqnrj2w30cwh6efk5"

ROUTER_PATH string = "gno.land/r/gnoswap/v1/router"
ROUTER_ADDR std.Address = "g1lm2l7tf49h3mykesct7rhfml30yx8dw5xrval7"

STAKER_PATH string = "gno.land/r/gnoswap/v1/staker"
STAKER_ADDR std.Address = "g1cceshmzzlmrh7rr3z30j2t5mrvsq9yccysw9nu"

GNS_PATH string = "gno.land/r/gnoswap/v1/gns"
GNS_ADDR std.Address = "g1jgqwaa2le3yr63d533fj785qkjspumzv22ys5m"

GNFT_PATH string = "gno.land/r/gnoswap/v1/gnft"
GNFT_ADDR std.Address = "g1wxv2rdfn53qc84nt3nn646f9yh3nly8lm7j89t"

WUGNOT_PATH string = "gno.land/r/demo/wugnot"
WUGNOT_ADDR std.Address = "g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6"

EMISSION_PATH string = "gno.land/r/gnoswap/v1/emission"
EMISSION_ADDR std.Address = "g10xg6559w9e93zfttlhvdmaaa0er3zewcr7nh20"

PROTOCOL_FEE_PATH string = "gno.land/r/gnoswap/v1/protocol_fee"
PROTOCOL_FEE_ADDR std.Address = "g1f7wpek7q67tkns27sw495u5yuu3a5wwjxw5l6l"

COMMUNITY_POOL_PATH string = "gno.land/r/gnoswap/v1/community_pool"
COMMUNITY_POOL_ADDR std.Address = "g100fnnlz5eh87p5hvwt8pf279lxaelm8k8md049"

GOV_XGNS_PATH string = "gno.land/r/gnoswap/v1/gov/xgns"
GOV_XGNS_ADDR std.Address = "g1wwh55uwzlz2zzr2qcvvxf83qhcvmx2t8779l9r"

GOV_STAKER_PATH string = "gno.land/r/gnoswap/v1/gov/staker"
GOV_STAKER_ADDR std.Address = "g17e3ykyqk9jmqe2y9wxe9zhep3p7cw56davjqwa"

GOV_GOVERNANCE_PATH string = "gno.land/r/gnoswap/v1/gov/governance"
GOV_GOVERNANCE_ADDR std.Address = "g17s8w2ve7k85fwfnrk59lmlhthkjdted8whvqxd"

COMMON_PATH string = "gno.land/r/gnoswap/v1/common"
COMMON_ADDR std.Address = "g14ytarn5u7h3xywygt8hzhs3m23frljz72ta9xk"

LAUNCHPAD_PATH string = "gno.land/r/gnoswap/v1/launchpad"
LAUNCHPAD_ADDR std.Address = "g122mau2lp2rc0scs8d27pkkuys4w54mdy2tuer3"

INIT_REGISTER_PATH string = "gno.land/r/g1er355fkjksqpdtwmhf5penwa82p0rhqxkkyhk5/v2/register_gnodev"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this address?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean INIT_REGISTER_PATH, right?
This is the address that was used to register token contracts before there was a grc20reg. It is currently deprecated and should be deleted. It will be updated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

)

// NUMBER
const (
// calculated by https://mathiasbynens.be/demo/integer-range
MAX_UINT8 string = "255"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't these be derived in-code?

UINT8_MAX uint8 = 255

MAX_UINT16 string = "65535"
UINT16_MAX uint16 = 65535

MAX_UINT32 string = "4294967295"
UINT32_MAX uint32 = 4294967295

MAX_UINT64 string = "18446744073709551615"
UINT64_MAX uint64 = 18446744073709551615

MAX_UINT128 string = "340282366920938463463374607431768211455"
MAX_UINT160 string = "1461501637330902918203684832716283019655932542975"
MAX_UINT256 string = "115792089237316195423570985008687907853269984665640564039457584007913129639935"

MAX_INT128 string = "170141183460469231731687303715884105727"
MAX_INT256 string = "57896044618658097711785492504343953926634992332820282019728792003956564819968"

// Tick Related
MIN_TICK int32 = -887272
MAX_TICK int32 = 887272

MIN_SQRT_RATIO string = "4295128739" // same as TickMathGetSqrtRatioAtTick(MIN_TICK)
MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" // same as TickMathGetSqrtRatioAtTick(MAX_TICK)

MIN_PRICE string = "4295128740" // MIN_SQRT_RATIO + 1
MAX_PRICE string = "1461446703485210103287273052203988822378723970341" // MAX_SQRT_RATIO - 1

// ETC
Q64 string = "18446744073709551616" // 2 ** 64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact this is exactly 2^64 and not 2^64-1 worries me of max limits being checked with this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aware of it, but we only use Q notation with uint256. (We don't check uint64 limit with that value)

Q96 string = "79228162514264337593543950336" // 2 ** 96

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not derive these values, ex through demo/uint256?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For better readability, all fixed numbers are defined in above file.

Q128 string = "340282366920938463463374607431768211456" // 2 ** 128

Q96_RESOLUTION uint = 96
Q128_RESOLUTION uint = 128
Q160_RESOLUTION uint = 160
)

// TIMESTAMP & DAY
const (
SECOND_IN_MILLISECOND = 1000

// in seconds
TIMESTAMP_MINUTE = 60

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIMESTAMP_HOUR = 3600

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIMESTAMP_DAY = 86400

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be moved to halving, as it's only used there. Doesn't need to be exported in that package

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIMESTAMP_YEAR = 31536000

DAY_PER_YEAR = 365

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives me the heebie-jeebies. What happens when it's a leap year?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been set to 365 days according to the policy.

)
Comment on lines +120 to +131
Copy link

@aeddi aeddi Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timestamp is not the right word as these are duration and It would be better to choose a formulation (e.g., DAYS_PER_YEAR) and adapt it for all the values above (MILLISECONDS_PER_SECOND, SECONDS_PER_MINUTE, etc.).

Like this:

// Time equivalence in different units
const (
	MILLISECONDS_PER_SECOND = 1000

	SECONDS_PER_MINUTE = 60
	SECONDS_PER_HOUR   = 3600
	SECONDS_PER_DAY    = 86400
	SECONDS_PER_YEAR   = 31536000

	DAYS_PER_YEAR = 365
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed unnecessary variable, and changed left ones // commit


// ETCs
const (
// REF: https://github.com/gnolang/gno/pull/2401#discussion_r1648064219
ZERO_ADDRESS std.Address = "g100000000000000000000000000000000dnmcnx"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please give me context on how this is used?
Because I've been looking through the usages and am starting to sweat

Copy link
Member

@r3v4s r3v4s Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, it's just place-holder for nil address.
Of course std.Address("") can be used but zero address seems more straightforward as contract context.

Decided to keep that value for now, @onlyhyde made issue to track it.

)
1 change: 1 addition & 0 deletions contract/p/gnoswap/consts/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/gnoswap/consts
39 changes: 39 additions & 0 deletions contract/p/gnoswap/gnsmath/_helper_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gnsmath

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this file start with _?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that the _ prefix was added to make it run first, since the test environment is affected by the execution order of test files.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire file can be replaced by already existing examples calls to uassert and urequire

Copy link
Member

@notJoon notJoon Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you've found traces of legacy code. These are functions that were created internally before the uassert package was added. We are gradually removing this as well.

removed in #489


import (
"testing"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please run gno fmt on the entire codebase 🙏

)

func shouldEQ(t *testing.T, got, expected interface{}) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with functions below

Suggested change
func shouldEQ(t *testing.T, got, expected interface{}) {
func shouldEqual(t *testing.T, got, expected interface{}) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have usassert and urequire in the standard examples

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I didn't know that, yes that's even better.

Copy link
Member

@notJoon notJoon Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the _helper_test file has been removed, and it has been modified to use uassert now. I think all comments related to this have been addressed.

https://github.com/gnoswap-labs/gnoswap/blob/00011ea7fb724ad21cc8f7747ea6883913999349/contract/p/gnoswap/gnsmath/bit_math_test.gno

if got != expected {
t.Errorf("got %v, expected %v", got, expected)
}
}

func shouldNEQ(t *testing.T, got, expected interface{}) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with functions below

Suggested change
func shouldNEQ(t *testing.T, got, expected interface{}) {
func shouldNotEqual(t *testing.T, got, expected interface{}) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aeddi I had to double take your comment

We can simplify this even more, by extracting the comparison to the caller and letting the caller decide. This way we can unify the shouldEQ and shouldNEQ into just func equals bool. The best option is to just use uassert and urequire

if got == expected {
t.Errorf("got %v, didn't expected %v", got, expected)
}
}

func shouldPanic(t *testing.T, f func()) {
notJoon marked this conversation as resolved.
Show resolved Hide resolved
defer func() {
if r := recover(); r == nil {
t.Errorf("expected panic")
}
}()
f()
}

func shouldPanicWithMsg(t *testing.T, f func(), msg string) {
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
} else {
if r != msg {
t.Errorf("excepted panic(%v), got(%v)", msg, r)
}
}
}()
f()
}
65 changes: 65 additions & 0 deletions contract/p/gnoswap/gnsmath/bit_math.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package gnsmath

import (
u256 "gno.land/p/gnoswap/uint256"
)

type bitShift struct {
bitPattern *u256.Uint
shift uint
}

func BitMathMostSignificantBit(x *u256.Uint) uint8 {
if x.IsZero() {
panic("BitMathMostSignificantBit: x should not be zero")
}

shifts := []bitShift{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this redefined every time?
These look like constants

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it has broken free

var (
msbShifts = []bitShift{
{u256.MustFromDecimal(Q128), 128}, // 2^128
{u256.MustFromDecimal(Q64), 64}, // 2^64
{u256.NewUint(0x100000000), 32}, // 2^32
{u256.NewUint(0x10000), 16}, // 2^16
{u256.NewUint(0x100), 8}, // 2^8
{u256.NewUint(0x10), 4}, // 2^4
{u256.NewUint(0x4), 2}, // 2^2
{u256.NewUint(0x2), 1}, // 2^1
}
lsbShifts = []bitShift{
{u256.MustFromDecimal(MAX_UINT128), 128},
{u256.MustFromDecimal(MAX_UINT64), 64},
{u256.MustFromDecimal(MAX_UINT32), 32},
{u256.MustFromDecimal(MAX_UINT16), 16},
{u256.MustFromDecimal(MAX_UINT8), 8},
{u256.NewUint(0xf), 4},
{u256.NewUint(0x3), 2},
{u256.NewUint(0x1), 1},
}
)

{u256.MustFromDecimal(Q128), 128}, // 2^128
{u256.MustFromDecimal(Q64), 64}, // 2^64
{u256.NewUint(0x100000000), 32},
{u256.NewUint(0x10000), 16},
{u256.NewUint(0x100), 8},
{u256.NewUint(0x10), 4},
{u256.NewUint(0x4), 2},
{u256.NewUint(0x2), 1},
}

r := uint8(0)
for _, s := range shifts {
if x.Gte(s.bitPattern) {
x = new(u256.Uint).Rsh(x, s.shift)
r += uint8(s.shift)
}
}

return r
}

func BitMathLeastSignificantBit(x *u256.Uint) uint8 {
if x.IsZero() {
panic("BitMathLeastSignificantBit: x should not be zero")
}

shifts := []bitShift{
{u256.MustFromDecimal(MAX_UINT128), 128},
{u256.MustFromDecimal(MAX_UINT64), 64},
{u256.MustFromDecimal(MAX_UINT32), 32},
{u256.MustFromDecimal(MAX_UINT16), 16},
{u256.MustFromDecimal(MAX_UINT8), 8},
{u256.NewUint(0xf), 4},
{u256.NewUint(0x3), 2},
{u256.NewUint(0x1), 1},
}

r := uint8(255)
for _, s := range shifts {
if new(u256.Uint).And(x, s.bitPattern).Gt(u256.Zero()) {
r -= uint8(s.shift)
} else {
x = new(u256.Uint).Rsh(x, s.shift)
}
}

return r
}
71 changes: 71 additions & 0 deletions contract/p/gnoswap/gnsmath/bit_math_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package gnsmath

import (
"testing"

u256 "gno.land/p/gnoswap/uint256"
)

func TestBitMathMostSignificantBit(t *testing.T) {
t.Run("0", func(t *testing.T) {
shouldPanic(
t,
func() {
BitMathMostSignificantBit(u256.Zero())
},
)
})

t.Run("1", func(t *testing.T) {
shouldEQ(t, BitMathMostSignificantBit(u256.One()), uint8(0))
})

t.Run("2", func(t *testing.T) {
shouldEQ(t, BitMathMostSignificantBit(u256.NewUint(2)), uint8(1))
})

t.Run("all powers of 2", func(t *testing.T) {
for i := 0; i < 256; i++ {
num := u256.Zero()
num.Lsh(u256.One(), uint(i))
shouldEQ(t, BitMathMostSignificantBit(num), uint8(i))
}
})

t.Run("uint256(-1)", func(t *testing.T) {
// BigNumber.from(2).pow(256).sub(1))
shouldEQ(t, BitMathMostSignificantBit(u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935")), uint8(255))
})
}

func TestBitMathLeastSignificantBit(t *testing.T) {
t.Run("0", func(t *testing.T) {
shouldPanic(
t,
func() {
BitMathLeastSignificantBit(u256.Zero())
},
)
})

t.Run("1", func(t *testing.T) {
shouldEQ(t, BitMathLeastSignificantBit(u256.One()), uint8(0))
})

t.Run("2", func(t *testing.T) {
shouldEQ(t, BitMathLeastSignificantBit(u256.NewUint(2)), uint8(1))
})

t.Run("all powers of 2", func(t *testing.T) {
for i := 0; i < 256; i++ {
num := u256.Zero()
num.Lsh(u256.One(), uint(i))
shouldEQ(t, BitMathLeastSignificantBit(num), uint8(i))
}
})

t.Run("uint256(-1)", func(t *testing.T) {
// BigNumber.from(2).pow(256).sub(1))
shouldEQ(t, BitMathLeastSignificantBit(u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935")), uint8(0))
})
}
16 changes: 16 additions & 0 deletions contract/p/gnoswap/gnsmath/consts.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gnsmath

const (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these constants defined in duplicate? (see file contract/p/gnoswap/consts/consts.gno)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gnsmath is package that we're willing to isolated,
@notJoon is working on refactor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some duplication is occasionally necessary. The gnsmath package must be created to have minimal dependencies with other packages except for p/demo or stdlib when possible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please derive these on the spot, why hardcode?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, they don't need to be exported I think

MAX_UINT8 string = "255"
MAX_UINT16 string = "65535"
MAX_UINT32 string = "4294967295"
MAX_UINT64 string = "18446744073709551615"
MAX_UINT128 string = "340282366920938463463374607431768211455"
MAX_UINT160 string = "1461501637330902918203684832716283019655932542975"
MAX_UINT256 string = "115792089237316195423570985008687907853269984665640564039457584007913129639935"
MAX_INT256 string = "57896044618658097711785492504343953926634992332820282019728792003956564819967"

Q64 string = "18446744073709551616" // 2 ** 64
Q96 string = "79228162514264337593543950336" // 2 ** 96
Q128 string = "340282366920938463463374607431768211456" // 2 ** 128
)
1 change: 1 addition & 0 deletions contract/p/gnoswap/gnsmath/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/gnoswap/gnsmath

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mod file not tidied.
Please check all gno mod files 🙏

Loading