303 lines
7.8 KiB
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
|
|
}
|