gcx/blockchain/genex-chain/x/evm/ante/compliance_ante_test.go

303 lines
7.8 KiB
Go

package ante
import (
"math/big"
"testing"
"time"
)
// ==============================
// OFAC Tests
// ==============================
func TestOFAC_BlocksSanctionedSender(t *testing.T) {
h := NewComplianceAnteHandler()
h.AddToOFACList("0xSanctioned")
result := h.AnteHandle("0xSanctioned", "0xNormal", big.NewInt(1000))
if result.Allowed {
t.Fatal("should block sanctioned sender")
}
if result.Reason == "" {
t.Fatal("should provide reason")
}
}
func TestOFAC_BlocksSanctionedReceiver(t *testing.T) {
h := NewComplianceAnteHandler()
h.AddToOFACList("0xSanctioned")
result := h.AnteHandle("0xNormal", "0xSanctioned", big.NewInt(1000))
if result.Allowed {
t.Fatal("should block sanctioned receiver")
}
}
func TestOFAC_AllowsCleanAddresses(t *testing.T) {
h := NewComplianceAnteHandler()
h.AddToOFACList("0xSanctioned")
result := h.AnteHandle("0xAlice", "0xBob", big.NewInt(1000))
if !result.Allowed {
t.Fatalf("should allow clean addresses, got reason: %s", result.Reason)
}
}
func TestOFAC_BulkUpdate(t *testing.T) {
h := NewComplianceAnteHandler()
addresses := []string{"0xA", "0xB", "0xC"}
h.UpdateOFACList(addresses)
if h.GetOFACListSize() != 3 {
t.Fatalf("expected 3, got %d", h.GetOFACListSize())
}
for _, addr := range addresses {
if !h.IsOFACSanctioned(addr) {
t.Fatalf("%s should be sanctioned", addr)
}
}
if h.IsOFACSanctioned("0xClean") {
t.Fatal("0xClean should not be sanctioned")
}
}
func TestOFAC_RemoveFromList(t *testing.T) {
h := NewComplianceAnteHandler()
h.AddToOFACList("0xAddr")
if !h.IsOFACSanctioned("0xAddr") {
t.Fatal("should be sanctioned")
}
h.RemoveFromOFACList("0xAddr")
if h.IsOFACSanctioned("0xAddr") {
t.Fatal("should no longer be sanctioned")
}
}
func TestOFAC_UpdateReplacesOldList(t *testing.T) {
h := NewComplianceAnteHandler()
h.AddToOFACList("0xOld")
h.UpdateOFACList([]string{"0xNew"})
if h.IsOFACSanctioned("0xOld") {
t.Fatal("0xOld should be removed after full update")
}
if !h.IsOFACSanctioned("0xNew") {
t.Fatal("0xNew should be in the new list")
}
}
// ==============================
// Travel Rule Tests
// ==============================
func TestTravelRule_BlocksLargeTransferWithoutRecord(t *testing.T) {
h := NewComplianceAnteHandler()
// $3,000 USDC = 3000 * 1e6 = 3,000,000,000
amount := new(big.Int).Mul(big.NewInt(3000), big.NewInt(1e6))
result := h.AnteHandle("0xAlice", "0xBob", amount)
if result.Allowed {
t.Fatal("should block large transfer without Travel Rule record")
}
}
func TestTravelRule_AllowsLargeTransferWithRecord(t *testing.T) {
h := NewComplianceAnteHandler()
h.RecordTravelRule("0xAlice", "0xBob")
amount := new(big.Int).Mul(big.NewInt(5000), big.NewInt(1e6))
result := h.AnteHandle("0xAlice", "0xBob", amount)
if !result.Allowed {
t.Fatalf("should allow large transfer with Travel Rule record, reason: %s", result.Reason)
}
}
func TestTravelRule_AllowsSmallTransferWithoutRecord(t *testing.T) {
h := NewComplianceAnteHandler()
// $2,999 — below threshold
amount := new(big.Int).Mul(big.NewInt(2999), big.NewInt(1e6))
result := h.AnteHandle("0xAlice", "0xBob", amount)
if !result.Allowed {
t.Fatal("should allow transfer below $3,000 without Travel Rule record")
}
}
func TestTravelRule_ExactThreshold(t *testing.T) {
h := NewComplianceAnteHandler()
// Exactly $3,000 — at threshold, should require record
amount := new(big.Int).Mul(big.NewInt(3000), big.NewInt(1e6))
result := h.AnteHandle("0xAlice", "0xBob", amount)
if result.Allowed {
t.Fatal("$3,000 exactly should require Travel Rule record")
}
}
func TestTravelRule_HasRecord(t *testing.T) {
h := NewComplianceAnteHandler()
if h.HasTravelRuleRecord("0xA", "0xB") {
t.Fatal("should not have record initially")
}
h.RecordTravelRule("0xA", "0xB")
if !h.HasTravelRuleRecord("0xA", "0xB") {
t.Fatal("should have record after recording")
}
// Direction matters
if h.HasTravelRuleRecord("0xB", "0xA") {
t.Fatal("reverse direction should not have record")
}
}
// ==============================
// Structuring Detection Tests
// ==============================
func TestStructuring_NoFlagForSingleSmallTransfer(t *testing.T) {
h := NewComplianceAnteHandler()
amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6)) // $500
result := h.AnteHandle("0xAlice", "0xBob", amount)
if result.Suspicious {
t.Fatal("single small transfer should not be flagged")
}
}
func TestStructuring_FlagsMultipleSmallTransfersExceedingThreshold(t *testing.T) {
h := NewComplianceAnteHandler()
// Record several small transfers totaling > $3,000
for i := 0; i < 5; i++ {
h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6))) // $500 each
}
// 6th transfer: cumulative = $3,000
amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6))
result := h.AnteHandle("0xAlice", "0xBob", amount)
if !result.Suspicious {
t.Fatal("cumulative small transfers exceeding $3,000 should be flagged as suspicious")
}
// But still allowed (not blocked)
if !result.Allowed {
t.Fatal("structuring should flag but not block")
}
}
func TestStructuring_DoesNotFlagIfSingleLargeTransfer(t *testing.T) {
h := NewComplianceAnteHandler()
// Record one large transfer > threshold
h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(4000), big.NewInt(1e6)))
// Another small transfer
amount := new(big.Int).Mul(big.NewInt(100), big.NewInt(1e6))
result := h.AnteHandle("0xAlice", "0xBob", amount)
// Not structuring because previous transfers include one >= threshold
if result.Suspicious {
t.Fatal("should not flag as structuring if previous transfers include large ones")
}
}
func TestStructuring_SlidingWindowExpiry(t *testing.T) {
h := NewComplianceAnteHandler()
// Override window to 1ms for testing
h.structuringWindow = 1 * time.Millisecond
h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(2000), big.NewInt(1e6)))
// Wait for window to expire
time.Sleep(5 * time.Millisecond)
amount := new(big.Int).Mul(big.NewInt(1500), big.NewInt(1e6))
result := h.AnteHandle("0xAlice", "0xBob", amount)
if result.Suspicious {
t.Fatal("expired window records should not contribute to structuring detection")
}
}
// ==============================
// Combined Scenario Tests
// ==============================
func TestCombined_OFACTakesPrecedenceOverTravelRule(t *testing.T) {
h := NewComplianceAnteHandler()
h.AddToOFACList("0xBad")
h.RecordTravelRule("0xBad", "0xGood")
amount := new(big.Int).Mul(big.NewInt(5000), big.NewInt(1e6))
result := h.AnteHandle("0xBad", "0xGood", amount)
if result.Allowed {
t.Fatal("OFAC should block even if Travel Rule record exists")
}
}
func TestCombined_StructuringPlusTravelRule(t *testing.T) {
h := NewComplianceAnteHandler()
// Record small transfers approaching threshold
for i := 0; i < 3; i++ {
h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(900), big.NewInt(1e6)))
}
// Small transfer that pushes cumulative over threshold
// But this single transfer is below threshold, so no Travel Rule needed
amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6))
result := h.AnteHandle("0xAlice", "0xBob", amount)
if !result.Allowed {
t.Fatal("below-threshold transfer should still be allowed")
}
if !result.Suspicious {
t.Fatal("should be flagged as suspicious structuring")
}
}
func TestConcurrency_SafeAccess(t *testing.T) {
h := NewComplianceAnteHandler()
done := make(chan struct{})
// Writer goroutine
go func() {
for i := 0; i < 100; i++ {
h.AddToOFACList("0xAddr")
h.RemoveFromOFACList("0xAddr")
h.RecordTravelRule("0xA", "0xB")
h.RecordTransfer("0xC", big.NewInt(1000))
}
done <- struct{}{}
}()
// Reader goroutine
go func() {
for i := 0; i < 100; i++ {
h.AnteHandle("0xAlice", "0xBob", big.NewInt(1000))
h.IsOFACSanctioned("0xAddr")
h.HasTravelRuleRecord("0xA", "0xB")
h.GetOFACListSize()
}
done <- struct{}{}
}()
<-done
<-done
}