Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions contract/r/gnoswap/router/v1/_helper_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ var (

registeredTestStore = false
registeredTestPool = false

mockInstance *routerV1
)

func TokenFaucet(t *testing.T, tokenPath string, to address) {
Expand Down Expand Up @@ -470,6 +472,10 @@ func initRouterTest(t *testing.T) {
registerPositionTest(t)

registeredTestStore = true

mockInstance = &routerV1{
store: mock.NewMockRouterStore(),
}
}

func initPoolTest(t *testing.T) {
Expand Down Expand Up @@ -511,3 +517,18 @@ func registerPositionTest(t *testing.T) {
testing.SetRealm(adminRealm)
pn.UpgradeImpl(cross, positionMockPath)
}

// mockInstanceSwapCallback is a helper function to call the SwapCallback function on the mock instance
func mockInstanceSwapCallback(
token0Path string,
token1Path string,
amount0Delta string,
amount1Delta string,
payer address,
) error {
// mock instance using a closure
return func(cur realm) error {
testing.SetRealm(testing.NewCodeRealm(routerPath))
return mockInstance.SwapCallback(token0Path, token1Path, amount0Delta, amount1Delta, payer)
}(cross)
}
9 changes: 9 additions & 0 deletions contract/r/gnoswap/router/v1/assert.gno
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@
}
}
}

func assertIsRouterV1(caller address) {
if caller != routerV1Addr {
panic(makeErrorWithDetails(

Check failure on line 109 in contract/r/gnoswap/router/v1/assert.gno

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an error instead of using panic for normal error conditions.

See more on https://sonarcloud.io/project/issues?id=gnoswap-labs_gnoswap&issues=AZq-mntA7Aq8WMylb5L3&open=AZq-mntA7Aq8WMylb5L3&pullRequest=1005
errInvalidInput,
ufmt.Sprintf("caller %s is not router v1(%s)", caller, routerV1Addr),
))
}
}
2 changes: 2 additions & 0 deletions contract/r/gnoswap/router/v1/consts.gno
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ var (
routerAddr = chain.PackageAddress("gno.land/r/gnoswap/router")
positionAddr = chain.PackageAddress("gno.land/r/gnoswap/position")
protocolFeeAddr = chain.PackageAddress("gno.land/r/gnoswap/protocol_fee")

routerV1Addr = chain.PackageAddress("gno.land/r/gnoswap/router/v1")
)
11 changes: 11 additions & 0 deletions contract/r/gnoswap/router/v1/swap_callback.gno
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package v1

import (
"chain/runtime"

i256 "gno.land/p/gnoswap/int256"
u256 "gno.land/p/gnoswap/uint256"

"gno.land/r/gnoswap/common"
"gno.land/r/gnoswap/halt"
)

// swapCallback implements the pool's SwapCallback interface.
Expand All @@ -15,11 +18,19 @@ import (
// 1. Flash swaps (receive tokens before paying)
// 2. Just-in-time token transfers
// 3. Complex multi-hop swaps without intermediate transfers
//
// Only callable from the router v1 implementation contract.
// It is only used when calling a pool swap function.
func (r *routerV1) SwapCallback(
token0Path, token1Path string,
amount0Delta, amount1Delta string,
payer address,
) error {
halt.AssertIsNotHaltedRouter()

caller := runtime.PreviousRealm().Address()
assertIsRouterV1(caller)

var tokenToPay string

amountToPay := i256.Zero()
Expand Down
196 changes: 196 additions & 0 deletions contract/r/gnoswap/router/v1/swap_callback_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package v1

import (
"testing"

"gno.land/p/nt/testutils"
"gno.land/p/nt/uassert"
)

// TestSwapCallback tests the SwapCallback function with various scenarios
func TestSwapCallback(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T) address // returns payer address
amount0Delta string
amount1Delta string
callerRealmPath string
shouldError bool
expectedErrorMsg string
}{
{
name: "success with token0 payment",
setupFunc: func(t *testing.T) address {
initRouterTest(t)
CreatePoolWithoutFee(t)
MakeMintPositionWithoutFee(t)
TokenFaucet(t, barPath, routerAddr)
TokenFaucet(t, bazPath, routerAddr)
return routerAddr
},
amount0Delta: "1000",
amount1Delta: "0",
callerRealmPath: "gno.land/r/gnoswap/router/v1",
shouldError: false,
},
{
name: "success with token1 payment",
setupFunc: func(t *testing.T) address {
initRouterTest(t)
CreatePoolWithoutFee(t)
MakeMintPositionWithoutFee(t)
TokenFaucet(t, barPath, routerAddr)
TokenFaucet(t, bazPath, routerAddr)
return routerAddr
},
amount0Delta: "0",
amount1Delta: "1000",
callerRealmPath: "gno.land/r/gnoswap/router/v1",
shouldError: false,
},
{
name: "success with both deltas zero",
setupFunc: func(t *testing.T) address {
initRouterTest(t)
CreatePoolWithoutFee(t)
return routerAddr
},
amount0Delta: "0",
amount1Delta: "0",
callerRealmPath: "gno.land/r/gnoswap/router/v1",
shouldError: false,
},
{
name: "success with user as payer",
setupFunc: func(t *testing.T) address {
initRouterTest(t)
CreatePoolWithoutFee(t)
MakeMintPositionWithoutFee(t)
testUser := testutils.TestAddress("testUser")
TokenFaucet(t, barPath, testUser)
TokenFaucet(t, bazPath, testUser)
TokenApprove(t, barPath, testUser, routerAddr, maxApprove)
TokenApprove(t, bazPath, testUser, routerAddr, maxApprove)
return testUser
},
amount0Delta: "1000",
amount1Delta: "0",
callerRealmPath: "gno.land/r/gnoswap/router/v1",
shouldError: false,
},
{
name: "fail with invalid caller",
setupFunc: func(t *testing.T) address {
initRouterTest(t)
CreatePoolWithoutFee(t)
return routerAddr
},
amount0Delta: "1000",
amount1Delta: "0",
callerRealmPath: "gno.land/r/unauthorized/contract",
shouldError: true,
expectedErrorMsg: "[GNOSWAP-ROUTER-005]",
},
{
name: "fail with insufficient balance",
setupFunc: func(t *testing.T) address {
initRouterTest(t)
CreatePoolWithoutFee(t)
MakeMintPositionWithoutFee(t)
poorUser := testutils.TestAddress("poorUser")
return poorUser
},
amount0Delta: "1000",
amount1Delta: "0",
callerRealmPath: "gno.land/r/gnoswap/router/v1",
shouldError: true,
expectedErrorMsg: "insufficient balance",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
payer := tt.setupFunc(t)

// Set caller realm
testing.SetRealm(testing.NewCodeRealm(tt.callerRealmPath))

// Execute
if tt.shouldError {
uassert.AbortsContains(t, tt.expectedErrorMsg, func() {
mockInstanceSwapCallback(
barPath,
bazPath,
tt.amount0Delta,
tt.amount1Delta,
payer,
)
})
} else {
err := mockInstanceSwapCallback(
barPath,
bazPath,
tt.amount0Delta,
tt.amount1Delta,
payer,
)
uassert.NoError(t, err)
}
})
}
}

// TestSwapCallback_AssertIsRouterV1 tests the assertIsRouterV1 function with various caller types
func TestSwapCallback_AssertIsRouterV1(t *testing.T) {
tests := []struct {
name string
callerAddress address
shouldError bool
expectedErrorMsg string
}{
{
name: "valid caller from router v1",
callerAddress: routerV1Addr,
shouldError: false,
},
{
name: "invalid caller from unauthorized contract",
callerAddress: testutils.TestAddress("unauthorized"),
shouldError: true,
expectedErrorMsg: "[GNOSWAP-ROUTER-005]",
},
{
name: "invalid caller from user realm",
callerAddress: testutils.TestAddress("maliciousUser"),
shouldError: true,
expectedErrorMsg: "[GNOSWAP-ROUTER-005]",
},
{
name: "invalid caller from different contract",
callerAddress: testutils.TestAddress("different"),
shouldError: true,
expectedErrorMsg: "[GNOSWAP-ROUTER-005]",
},
{
name: "invalid caller from pool contract",
callerAddress: poolAddr,
shouldError: true,
expectedErrorMsg: "[GNOSWAP-ROUTER-005]",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Execute and verify
if tt.shouldError {
uassert.PanicsContains(t, tt.expectedErrorMsg, func() {
assertIsRouterV1(tt.callerAddress)
})
} else {
// Should not panic
assertIsRouterV1(tt.callerAddress)
}
})
}
}
Loading