mirror of
https://github.com/luxfi/bank.git
synced 2026-10-30 17:02:07 +00:00
Every chain test skipped for want of a chain, the fifteen adversarial ones
included, so none of them had ever executed. Against a locally deployed
protocol four failed, and all four were stale rather than findings: three
were exploit demonstrations still asserting that the exploit lands, and
one had its setup invalidated by the fix for another.
- the treasury drain asserted value reached the sink. The ledger
reserves before the chain moves now, so it asserts the opposite: the
refusal arrives with the chain untouched, which is the only thing that
tells a bank that checked first from one that got lucky.
- the LUX vault asserted a customer's coin could not be deposited. The
market wraps on the way in, and reads the wrapper's own name off the
chain before value follows it there.
- the receipt asserted one response carried two network names. Both read
the backend now.
- gas funding seeded two broke customers and expected them to pay. The
treasury covers gas and only gas — covering the value is what funded
the drain — so they hold what they are sending. That is what surfaced
the shortfall bug fixed alongside this.
The borrow ceiling test seeded the market's collateral token directly,
which only exists by wrapping, so the transfer underflowed. It seeds the
coin and lets the deposit wrap, takes an index of its own, and unwinds
what it finds and what it leaves — one chain outlives every run, and a
position left open is read as the next run's own.
Fifteen adversarial tests, three consecutive clean runs on one chain.
Co-authored-by: Hanzo Dev <[email protected]>
80 lines
2.7 KiB
Go
80 lines
2.7 KiB
Go
package bank
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tests"
|
|
"github.com/luxfi/bank/collections"
|
|
)
|
|
|
|
// An Earn movement and the position it changes are one act. Written separately,
|
|
// a position that failed to save after the money had already settled cost
|
|
// somebody every way round: a deposit debited without crediting the collateral,
|
|
// a borrow paid out without recording the debt, a repayment taken without
|
|
// reducing it.
|
|
//
|
|
// The failure is provoked with a hook, because that is the shape the real one
|
|
// takes — something refusing the write after the balance has moved. What the
|
|
// test asserts is that nothing moved at all.
|
|
func TestAnEarnMovementAndItsPositionMoveTogether(t *testing.T) {
|
|
for _, act := range []struct{ verb, body string }{
|
|
{"deposit", `{"vault":"stlux","amount":1000000}`},
|
|
{"borrow", `{"vault":"stlux","amount":1000000}`},
|
|
} {
|
|
t.Run(act.verb, func(t *testing.T) {
|
|
// This is the ledger path's atomicity. With a chain configured the
|
|
// movement goes to the market instead, where the position the
|
|
// contract reports back is the position and none of this applies —
|
|
// so the path is pinned rather than left to the environment.
|
|
t.Setenv("BANK_CHAIN_RPC", "")
|
|
evmMu.Lock()
|
|
evmInst, evmFrom = nil, ""
|
|
evmMu.Unlock()
|
|
t.Cleanup(func() {
|
|
evmMu.Lock()
|
|
evmInst, evmFrom = nil, ""
|
|
evmMu.Unlock()
|
|
})
|
|
|
|
app := newBankApp(t)
|
|
_, token := seedPrincipal(t, app)
|
|
acct := primaryAccount(app, principalID(t, app))
|
|
if acct == nil {
|
|
t.Fatal("no account provisioned")
|
|
}
|
|
|
|
// Refuse every position write, after the movement has settled.
|
|
app.OnRecordUpdate(collections.PositionCollectionName).BindFunc(func(e *core.RecordEvent) error {
|
|
return errors.New("position store is unavailable")
|
|
})
|
|
app.OnRecordCreate(collections.PositionCollectionName).BindFunc(func(e *core.RecordEvent) error {
|
|
return errors.New("position store is unavailable")
|
|
})
|
|
|
|
before := availableOf(t, app, acct.Id, "LUX")
|
|
if before == 0 {
|
|
t.Fatal("the account holds no LUX, so this proves nothing")
|
|
}
|
|
|
|
run(t, app, tests.ApiScenario{
|
|
Name: act.verb + " is refused whole when the position cannot be written",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/bank/earn/" + act.verb,
|
|
Body: strings.NewReader(act.body),
|
|
Headers: map[string]string{"Authorization": token, "Content-Type": "application/json"},
|
|
ExpectedStatus: 500,
|
|
ExpectedContent: []string{"position"},
|
|
})
|
|
|
|
if after := availableOf(t, app, acct.Id, "LUX"); after != before {
|
|
t.Errorf("the balance moved by %d on a %s that failed — the money settled and the position did not",
|
|
after-before, act.verb)
|
|
}
|
|
})
|
|
}
|
|
}
|